Skip to content

🔧(evals) add run_eval management command - #481

Open
maxenceh wants to merge 1 commit into
mainfrom
maxenceh/setup-eval-llm
Open

🔧(evals) add run_eval management command#481
maxenceh wants to merge 1 commit into
mainfrom
maxenceh/setup-eval-llm

Conversation

@maxenceh

@maxenceh maxenceh commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Purpose

This PR introduces a behavioral eval framework for ConversationAgent. Unlike unit tests, these evals exercise LLM behaviour end-to-end:

  • does the model call the right tool (or none)?
  • does it respect system instructions and avoid known bad patterns?
  • are answers grounded in retrieved context?

A failing eval means a documented behaviour has regressed. The system is designed to grow: adding a new dataset requires one YAML file, one config file, and a registry entry.

Proposal

Running evals

  • make eval / make eval-debug — runs evals inside Docker; debug mode exposes debugpy on port 5678 for VS Code remote attach
  • Git metadata (commit, branch, dirty) is injected from the host via EVAL_GIT_* env vars (Docker has no .git mount)
  • run_evals management command: --dataset, --case, --runs, --verbose, --no-llm-judge, --save, --comment, --include-outputs
  • Tested model = LLM_DEFAULT_MODEL_HRID; judge = LLM_EVAL_JUDGE_MODEL_HRID (falls back to default; warns when judge == tested model)
  • --save is rejected when combined with --case (partial runs would register coverage gaps vs baseline)

Datasets (4)

Dataset What it tests
url_hallucination Never invents http(s):// URLs; only cites URLs from tool output or user message
faithfulness_rag Answers grounded in RAG chunks; must call document_search_rag, must not call web_search
incertitude On high-stakes French service-public questions, asks for missing personal context instead of guessing
tool_selection Calls the right tool (web_search, self_documentation, document_search_rag, summarize) or none — includes adversarial French phrasing

self_documentation is covered as cases within tool_selection, not as a standalone dataset.

RAG/summarize cases use inputs.requires_documents: true + a fake document listing. Per-case stub payloads are staged via tool_stub_responses.py (contextvar) so the model must actually call tools.

Evaluators & agent wiring

  • EvalConfig registry — each dataset declares rubric, evaluators, and optional make_task_fn / agent_class
  • UrlRegexEvaluator — deterministic URL check; strips trailing punctuation; reads runtime tool returns via set_eval_attribute
  • HasNoMatchingSpan — correct "tool was NOT called" check (avoids not_(HasMatchingSpan) false positives)
  • production_agent.py — builds agents with production tool wiring; stubs external calls (no DB/API)
  • Per-case YAML evaluators are merged with config evaluators (not replaced)

Persistence, baselines & comparison

  • --save writes JSON runs under chat/evals/runs/ (gitignored) with git metadata, dataset hashes, per-case avg scores, repeat pass rates
  • overall_pass_rate weighted by case count (not per-dataset average)
  • make eval-baseline — promotes a run to committed baseline (baselines/main.json + main_run.json)
  • make eval-compare — CLI diff with regression detection and coverage-gap handling (--fail-on-regression)
  • make eval-reset — wipe local runs/baselines/dashboard (--keep-baselines, --keep-dashboard, --dry-run)

HTML dashboard

  • make eval-dashboard — self-contained HTML from template.html + embedded run data
  • Compare runs A/B with per-dataset pass-rate summary (including total), filters (dataset / case / changes only), per-case deltas with failure reasons
  • Coverage gaps aligned with compare.py semantics
  • Embeds latest 20 runs + baseline runs; dataset/case descriptions from YAML

Albert API compatibility

  • AlbertOpenAIChatModel._validate_completion — normalizes Albert non-stream responses for pydantic-ai / pydantic_evals:
    • tool_calls[].typefunction
    • non-standard object / non-list choices on multi-turn tool-call conversations
  • Required for evals and LLM judge to work with Albert's OpenAI-compatible API

Docs & tests

  • chat/evals/README.md — structure, datasets, commands, how to add a dataset
  • Unit tests for storage, compare, report aggregation, dashboard generation, production agent wiring, tool stubs, reset

Test plan

  • make eval EVAL_ARGS="--dataset url_hallucination" passes on current default model
  • make eval EVAL_ARGS="--dataset tool_selection --runs 3" — repeat aggregation looks sane
  • make eval EVAL_ARGS='--save --comment "test"' then make eval-compare — no unexpected regressions vs baseline
  • make eval-dashboard — open dashboard.html, verify A/B summary and case drill-down
  • make eval EVAL_ARGS="--no-llm-judge" works when Albert structured output is unavailable
  • bin/pytest chat/tests/evals/ green

Summary by CodeRabbit

  • New Features
    • Added behavioral evaluation tooling with configurable datasets, repeat runs, saved runs, baselines, run-to-run comparisons, and regression detection.
    • Added a self-contained dashboard to filter and compare evaluation results.
    • Added eval support for URL hallucinations, retrieval faithfulness, uncertainty handling, and tool-selection behavior.
    • Added management commands for running, debugging, baselining, comparing, dashboard generation, and resetting.
  • Bug Fixes
    • Improved normalization/validation for malformed tool-call completion payloads and evaluation tool-call extraction.
  • Documentation
    • Expanded end-to-end documentation for behavioral evals and added dashboard guidance.
  • Chores
    • Updated shared tool guidance/prompts for correct routing and safer behavior.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@camilleAND, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e3034a66-ac08-4baf-960a-e2eb645df93b

📥 Commits

Reviewing files that changed from the base of the PR and between 18e7ee7 and f63a865.

⛔ Files ignored due to path filters (1)
  • src/backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (54)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/.pylintrc
  • src/backend/chat/agents/base.py
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_configs.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/tools/descriptions.py
  • src/backend/conversations/settings.py
  • src/backend/pyproject.toml

Walkthrough

This PR adds a behavioral evaluation framework with datasets, production-style agent wiring, run persistence, baseline comparison, dashboard generation, management commands, documentation, and tests. It also normalizes malformed Albert completion payloads before validation.

Changes

Behavioral evals

Layer / File(s) Summary
Eval contracts and dataset configurations
src/backend/chat/evals/..., src/backend/chat/evals/datasets/*, src/backend/chat/evals/README.md
Adds eval schemas, YAML-backed configuration, custom evaluators, four registered datasets, documentation, and validation tests.
Production agent wiring and tool stubbing
src/backend/chat/evals/production_agent.py, src/backend/chat/evals/tool_stub_responses.py, src/backend/chat/evals/configs/*, src/backend/chat/tests/evals/*
Adds production-like eval sessions, tool replacement, context-scoped stubs, RAG/tool-selection task factories, and related tests.
Eval execution and persistence
src/backend/chat/management/commands/run_evals.py, src/backend/chat/evals/report_builder.py, src/backend/chat/evals/storage.py, src/backend/chat/management/commands/reset_evals.py, Makefile
Adds evaluation execution, repeat aggregation, saved run records, git metadata, baselines, cleanup commands, Make targets, and a committed baseline snapshot.
Comparison and dashboard
src/backend/chat/evals/compare.py, src/backend/chat/evals/dashboard.py, src/backend/chat/evals/dashboard/template.html, src/backend/chat/management/commands/compare_evals.py, src/backend/chat/management/commands/generate_eval_dashboard.py
Adds run comparison classifications, coverage-gap handling, dashboard payload generation, interactive HTML rendering, and comparison/dashboard commands.
Repository and settings integration
.gitignore, .codespellrc, src/backend/conversations/settings.py, src/backend/pyproject.toml, CHANGELOG.md
Adds eval artifact rules, dependency configuration, judge-model defaults, anti-fabrication instructions, and changelog documentation.

Albert completion normalization

Layer / File(s) Summary
Completion payload normalization
src/backend/chat/providers/albert_models.py, src/backend/chat/tests/agents/test_albert_models.py
Normalizes Albert completion object, choices, and tool-call fields before Pydantic validation and tests the supported payload variants.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: providenz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is clearly related to the main change, though it mentions only the run_evals command and not the broader eval framework.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch maxenceh/setup-eval-llm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maxenceh
maxenceh force-pushed the maxenceh/setup-eval-llm branch 3 times, most recently from 3baa473 to 2b2ad04 Compare May 19, 2026 12:33
@maxenceh maxenceh changed the title Maxenceh/setup eval llm 🔧(evals) add run_eval management command May 19, 2026
@maxenceh
maxenceh marked this pull request as ready for review May 19, 2026 12:39
coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/backend/chat/evals/README.md (2)

9-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced code block.

The code fence at line 9 still lacks a language identifier, triggering markdownlint MD040. Adding text or plaintext will resolve the linting warning.

📝 Proposed fix
-```
+```text
 chat/evals/
 ├── configs/
 │   ├── __init__.py          # REGISTRY — maps dataset name → EvalConfig
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/README.md` around lines 9 - 23, The fenced code block
in src/backend/chat/evals/README.md is missing a language identifier (triggering
MD040); update the opening triple-backtick of the directory tree block to
include a language token such as ```text or ```plaintext so the block becomes a
proper fenced code block with a language identifier; locate the block showing
"chat/evals/" and change its opening fence accordingly.

29-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify that URLs from user input are also allowed.

The description states URLs must come "only from tool output," but if the evaluator also accepts URLs present in user_message (as flagged in previous review), this description is misleading. Consider updating to "only uses URLs from tool output or user input" to accurately reflect the evaluator behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/README.md` at line 29, Update the README description
for the `url_hallucination` check to reflect that allowed URLs may come from
both tool outputs and user input: change the phrase "only uses URLs from tool
output" to "only uses URLs from tool output or user input" and ensure the
`UrlRegexEvaluator` and `LLMJudge` references remain intact so readers know
those components perform regex and semantic checks respectively (refer to
`url_hallucination`, `UrlRegexEvaluator`, and `LLMJudge` in the same line).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/management/commands/run_evals.py`:
- Around line 123-126: The _run_dataset function currently has a bool return
annotation and docstring but actually returns an EvaluationReport at the end
(see the return of EvaluationReport around the block that used to be line 145);
update the function signature and docstring of _run_dataset to reflect that it
returns an EvaluationReport (not bool), adjust the type hint to
EvaluationReport, and update the docstring to describe the EvaluationReport
contents so callers and type checks are correct.

---

Duplicate comments:
In `@src/backend/chat/evals/README.md`:
- Around line 9-23: The fenced code block in src/backend/chat/evals/README.md is
missing a language identifier (triggering MD040); update the opening
triple-backtick of the directory tree block to include a language token such as
```text or ```plaintext so the block becomes a proper fenced code block with a
language identifier; locate the block showing "chat/evals/" and change its
opening fence accordingly.
- Line 29: Update the README description for the `url_hallucination` check to
reflect that allowed URLs may come from both tool outputs and user input: change
the phrase "only uses URLs from tool output" to "only uses URLs from tool output
or user input" and ensure the `UrlRegexEvaluator` and `LLMJudge` references
remain intact so readers know those components perform regex and semantic checks
respectively (refer to `url_hallucination`, `UrlRegexEvaluator`, and `LLMJudge`
in the same line).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0e639ce5-50e8-4a14-a9a1-c4f2d02fb4e4

📥 Commits

Reviewing files that changed from the base of the PR and between 2b2ad04 and 1b29c2f.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • Makefile
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/datasets/self_documentation.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/management/__init__.py
  • src/backend/chat/management/commands/__init__.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/backend/chat/evals/datasets/self_documentation.yaml
  • Makefile
  • src/backend/chat/evals/init.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/configs/init.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/evals/evaluators/init.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/evals/configs/self_documentation.py

Comment thread src/backend/chat/management/commands/run_evals.py Outdated
@maxenceh
maxenceh force-pushed the maxenceh/setup-eval-llm branch from 1b29c2f to 4235b6a Compare May 19, 2026 15:10
@sonarqubecloud

This comment was marked as outdated.

@maxenceh
maxenceh requested a review from providenz May 25, 2026 08:41
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 4235b6a to f736c54 Compare June 24, 2026 13:08
Comment thread src/backend/chat/evals/dashboard/dashboard.html Fixed
Comment thread src/backend/chat/evals/dashboard/dashboard.html Fixed
Comment thread src/backend/chat/evals/dashboard/dashboard.html Fixed
Comment thread src/backend/chat/evals/dashboard/dashboard.html Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (5)
src/backend/chat/evals/compare.py (2)

158-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split report formatting into smaller render helpers.

SonarCloud reports format_comparison above the allowed cognitive-complexity threshold. Extract dataset header rendering, case-change rendering, and score/reason rendering into helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/compare.py` around lines 158 - 226, The
format_comparison function has too much nested rendering logic and exceeds the
cognitive-complexity limit. Refactor it by extracting the dataset header/status
lines, the case-change block, and the score/reason rendering into small helper
functions near format_comparison in compare.py, then have format_comparison
orchestrate those helpers while preserving the current output format.

Source: Linters/SAST tools


76-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split compare_runs to satisfy the failing complexity gate.

SonarCloud reports this function above the allowed cognitive-complexity threshold. Extracting dataset lookup/warning handling and per-case delta construction into helpers should unblock the check without changing behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/compare.py` around lines 76 - 143, The compare_runs
function is exceeding the cognitive-complexity limit; split its dataset-level
warning/lookup logic and case-delta building into small helpers. Refactor around
compare_runs, _case_map, DatasetComparison, and CaseChange so the main function
only orchestrates comparison while preserving the same behavior and warnings.

Source: Linters/SAST tools

src/backend/chat/management/commands/run_evals.py (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove commented-out config lines.

These commented settings overrides are dead artifacts and flagged by static analysis.

🧹 Proposed fix
-# settings.LLM_DEFAULT_MODEL_HRID = "etalab-plateform-mistral-medium-2508"
-# settings.LANGFUSE_ENABLED = False
-
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` around lines 20 - 21,
Remove the commented-out settings overrides from run_evals.py; these are dead
artifacts and should not remain in the command module. Delete the commented
lines near the settings references in run_evals and keep the file focused on
active configuration logic only.

Source: Linters/SAST tools

src/backend/chat/tests/evals/test_production_agent.py (1)

83-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test depends on, and masks, the process-global caching behavior.

This test asserts identity-of-cached-state across two builder calls, which only passes because of the module-level globals in production_agent.py. If those globals are reset for isolation (see the related comment on production_agent.py), this test would need both builds to run within the same un-reset scope. Worth making the dependency explicit (e.g., build both inside one logical step) so the test intent survives an isolation fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_production_agent.py` around lines 83 - 90,
This test is relying on the process-global cache in
build_production_agent_service, so make the dependency explicit by grouping the
two service builds in one un-reset scope or shared step. Update
test_eval_session_reuses_same_user_and_conversation to clearly exercise the same
cached session behavior for both service_a and service_b, using the
build_production_agent_service helper and the user/conversation assertions, so
it still reflects the intended reuse if module-level globals in
production_agent.py are later isolated or reset.
src/backend/chat/evals/configs/faithfulness_rag.py (1)

82-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_CURRENT_CHUNKS is set but never reset between cases.

The contextvar is overwritten on each run_agent call, so correctness relies on every case setting it before the stub reads it. This holds today (sequential, max_concurrency=1), but if a case ever has tool_output=None/empty while the tool still fires, it will silently fall back to the previous case's chunks only if set is skipped — here set(None) is always called, so the fallback string handles it. No bug, but a try/finally reset (or ContextVar.reset via the token) would make this robust against future concurrency changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/configs/faithfulness_rag.py` around lines 82 - 86, The
run_agent helper in faithfulness_rag.py sets _CURRENT_CHUNKS for each eval case
but never restores the previous ContextVar state, so make it robust by capturing
the token from _CURRENT_CHUNKS.set(...) and resetting it in a try/finally around
agent.run. Update the run_agent function to use ContextVar.reset via the token
so the stub state cannot leak across future concurrent or failed runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/compare.py`:
- Around line 89-94: The comparison logic in compare_evals is treating missing
after-run coverage as only a warning, so incomplete candidate runs can still
pass when fail-on-regression is enabled. Update the dataset/case handling in
compare.py, especially the comparison path around the missing-dataset check and
the case-level logic, so missing items from the after run are recorded as
regression-like failures (or otherwise surfaced through the same failure path as
regressions). Make sure the Comparison object and compare_evals
--fail-on-regression flow use these coverage failures when deciding the final
exit status.

In `@src/backend/chat/evals/dashboard.py`:
- Line 39: The JSON string used for embedding dashboard payloads into the script
tag is currently taken directly from json.dumps(), which can still contain
script-breaking sequences like </script>. Update the payload
serialization/embedding path in the dashboard rendering code so the serialized
JSON is escaped for safe insertion into HTML script content before it is
interpolated and later parsed with JSON.parse(). Apply the same fix anywhere
else in this module that emits the same pattern.
- Around line 318-347: The saved eval values are being inserted into HTML
without escaping, so update the render paths that build `innerHTML` to sanitize
all run-artifact fields before interpolation. In `runParamsHtml` and the other
affected dashboard renderers, apply `escapeHtml()` consistently to values like
`run_id`, model/dataset/branch metadata, evaluator names, and LLM reasons, or
switch those fragments to DOM-based element creation before assignment. Use the
existing helpers such as `escapeHtml`, `runNote`, and the related HTML-building
functions in this module to ensure every saved field is treated as untrusted.

In `@src/backend/chat/evals/evaluators/url_regex.py`:
- Line 10: The URL detection regex in URL regex compilation is too strict about
the scheme and misses uppercase or mixed-case variants like HTTP://. Update the
pattern used by _URL_RE in the URL regex evaluator to match the scheme
case-insensitively, either by making the scheme portion explicitly flexible or
by using the appropriate regex flag, while keeping the rest of the URL-matching
behavior unchanged.

In `@src/backend/chat/evals/production_agent.py`:
- Around line 29-61: The eval-session caches in _get_eval_session_user() and
_get_eval_session_conversation() persist across the whole Python process, so
stale User and ChatConversation objects can survive between tests after
django_db rolls back the database rows. Add test isolation by resetting
EVAL_SESSION_USER and EVAL_SESSION_CONVERSATION to None between tests,
preferably with an autouse fixture, or clear them from
build_production_agent_service() when running in test mode. Keep the fix
localized to the eval-session globals in production_agent.py so each test
rebuilds fresh objects.

In `@src/backend/chat/evals/report_builder.py`:
- Around line 42-45: The `_repeat_passed` helper is using exact float equality
against 1.0, which can incorrectly fail near-perfect numeric evaluator scores.
Update the comparison in `_repeat_passed` to use an explicit pass threshold or
tolerance for non-boolean scores, while still preserving the intended behavior
for boolean-style assertions. Keep the logic localized to `_repeat_passed` so
the evaluator result handling remains easy to find and adjust.

In `@src/backend/chat/evals/storage.py`:
- Around line 94-98: The run ID generated by make_run_id only has second-level
timestamp precision, so repeated calls within the same second can collide and
cause save_run to overwrite earlier files or dedupe them in the index. Update
make_run_id to include finer-grained uniqueness, such as sub-second timestamp
precision or a short random suffix, while keeping the existing git_short/nogit
suffix behavior intact.
- Line 37: The exception handling in storage.py is using Python 2-style syntax,
which causes a SyntaxError and prevents the module from importing. Update the
except clauses in the affected error-handling blocks around the subprocess and
file lookup logic to use a tuple of exception types with Python 3 syntax, and
apply the same fix to both matching handlers so run_evals, save_run, and related
code can import successfully.

In `@src/backend/chat/tests/agents/test_albert_models.py`:
- Around line 245-250: The test for Albert model completion validation currently
only asserts choices normalization and does not verify that the response object
is coerced to chat.completion. Update
test_validate_completion_normalizes_non_standard_object to explicitly assert the
normalized object field on the result from _validate_completion, while keeping
the existing choices assertion, so the contract is locked in by the test.

---

Nitpick comments:
In `@src/backend/chat/evals/compare.py`:
- Around line 158-226: The format_comparison function has too much nested
rendering logic and exceeds the cognitive-complexity limit. Refactor it by
extracting the dataset header/status lines, the case-change block, and the
score/reason rendering into small helper functions near format_comparison in
compare.py, then have format_comparison orchestrate those helpers while
preserving the current output format.
- Around line 76-143: The compare_runs function is exceeding the
cognitive-complexity limit; split its dataset-level warning/lookup logic and
case-delta building into small helpers. Refactor around compare_runs, _case_map,
DatasetComparison, and CaseChange so the main function only orchestrates
comparison while preserving the same behavior and warnings.

In `@src/backend/chat/evals/configs/faithfulness_rag.py`:
- Around line 82-86: The run_agent helper in faithfulness_rag.py sets
_CURRENT_CHUNKS for each eval case but never restores the previous ContextVar
state, so make it robust by capturing the token from _CURRENT_CHUNKS.set(...)
and resetting it in a try/finally around agent.run. Update the run_agent
function to use ContextVar.reset via the token so the stub state cannot leak
across future concurrent or failed runs.

In `@src/backend/chat/management/commands/run_evals.py`:
- Around line 20-21: Remove the commented-out settings overrides from
run_evals.py; these are dead artifacts and should not remain in the command
module. Delete the commented lines near the settings references in run_evals and
keep the file focused on active configuration logic only.

In `@src/backend/chat/tests/evals/test_production_agent.py`:
- Around line 83-90: This test is relying on the process-global cache in
build_production_agent_service, so make the dependency explicit by grouping the
two service builds in one un-reset scope or shared step. Update
test_eval_session_reuses_same_user_and_conversation to clearly exercise the same
cached session behavior for both service_a and service_b, using the
build_production_agent_service helper and the user/conversation assertions, so
it still reflects the intended reuse if module-level globals in
production_agent.py are later isolated or reset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5502ee0-230e-401b-a1cb-3575b2c3c3e2

📥 Commits

Reviewing files that changed from the base of the PR and between 1b29c2f and f736c54.

📒 Files selected for processing (42)
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/dashboard.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/self_documentation.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/2026-06-17T14-56-06Z_nogit.json
  • src/backend/chat/evals/runs/2026-06-17T15-00-29Z_nogit.json
  • src/backend/chat/evals/runs/2026-06-18T14-29-12Z_nogit.json
  • src/backend/chat/evals/runs/2026-06-18T14-38-04Z_nogit.json
  • src/backend/chat/evals/runs/index.json
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_storage.py
✅ Files skipped from review due to trivial changes (15)
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • .gitignore
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/evals/runs/index.json
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/runs/2026-06-17T15-00-29Z_nogit.json
  • src/backend/chat/evals/runs/2026-06-18T14-29-12Z_nogit.json
  • CHANGELOG.md
  • src/backend/chat/evals/init.py
  • src/backend/chat/evals/runs/2026-06-17T14-56-06Z_nogit.json
  • src/backend/chat/evals/runs/2026-06-18T14-38-04Z_nogit.json
  • src/backend/chat/evals/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/evaluators/init.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/evals/datasets/self_documentation.yaml

Comment thread src/backend/chat/evals/compare.py Outdated
Comment thread src/backend/chat/evals/dashboard.py Outdated
Comment thread src/backend/chat/evals/dashboard.py Outdated
Comment thread src/backend/chat/evals/evaluators/url_regex.py Outdated
Comment thread src/backend/chat/evals/production_agent.py
Comment thread src/backend/chat/evals/report_builder.py Outdated
Comment thread src/backend/chat/evals/storage.py
Comment thread src/backend/chat/evals/storage.py
Comment thread src/backend/chat/tests/agents/test_albert_models.py
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from f736c54 to a698088 Compare June 25, 2026 10:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/backend/chat/evals/compare.py (1)

112-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Reduce cognitive complexity flagged by SonarCloud (compare_runs 28, format_comparison 42).

Both functions exceed the project's allowed cognitive complexity and are failing the SonarCloud gate. Consider extracting the missing-after-dataset branch (Lines 130-152) and the per-case diff (Lines 174-206) into helpers, and splitting format_comparison's dataset/case rendering into smaller functions, to bring complexity under threshold without behavior change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/compare.py` around lines 112 - 210, The SonarCloud
gate is failing because compare_runs and format_comparison are too complex.
Refactor compare_runs by extracting the missing-after-dataset handling and the
per-case comparison/diff logic into small helpers, using the existing
compare_runs, _case_map, and _missing_after_case_change flow as anchors. Then
split format_comparison into separate dataset-level and case-level rendering
helpers so each function’s branching is reduced without changing behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/backend/chat/evals/compare.py`:
- Around line 112-210: The SonarCloud gate is failing because compare_runs and
format_comparison are too complex. Refactor compare_runs by extracting the
missing-after-dataset handling and the per-case comparison/diff logic into small
helpers, using the existing compare_runs, _case_map, and
_missing_after_case_change flow as anchors. Then split format_comparison into
separate dataset-level and case-level rendering helpers so each function’s
branching is reduced without changing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 473e3a31-0073-4a44-af70-dbc85465c519

📥 Commits

Reviewing files that changed from the base of the PR and between f736c54 and a698088.

📒 Files selected for processing (16)
  • Makefile
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/dashboard.html
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/storage.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_storage.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/evals/dashboard.py
  • Makefile
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/dashboard/dashboard.html

@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from a698088 to e5baf5b Compare June 25, 2026 12:51
Comment thread src/backend/chat/evals/dashboard/template.html Fixed
Comment thread src/backend/chat/evals/dashboard/template.html Fixed
Comment thread src/backend/chat/evals/dashboard/template.html Fixed
Comment thread src/backend/chat/evals/dashboard/template.html Fixed
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch 6 times, most recently from bdfe3cb to ad951fa Compare July 1, 2026 12:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
src/backend/chat/evals/storage.py (3)

41-54: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Same invalid except syntax repeated in _run_git.

Identical issue as _git_root — apply the same tuple fix here.

🐛 Proposed fix
-    except subprocess.CalledProcessError, FileNotFoundError:
+    except (subprocess.CalledProcessError, FileNotFoundError):
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 41 - 54, The _run_git helper
still uses invalid multi-exception syntax in its except block, matching the same
issue as _git_root. Update the exception handling in _run_git to catch both
subprocess.CalledProcessError and FileNotFoundError using the correct tuple
form, keeping the fallback return None behavior unchanged. Use the _run_git
function name to locate the fix.

25-38: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

SyntaxError: invalid Python 2 multi-exception syntax breaks module import.

except subprocess.CalledProcessError, FileNotFoundError: is invalid in Python 3 — must be a tuple. This raises SyntaxError at import time, breaking the entire chat.evals.storage module (and by extension run_evals, save_run). A prior review flagged this same issue and it was reported as addressed, but the code shown here still has the bug — please re-verify the fix landed on this branch.

🐛 Proposed fix
-    except subprocess.CalledProcessError, FileNotFoundError:
+    except (subprocess.CalledProcessError, FileNotFoundError):
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 25 - 38, The `_git_root`
helper still uses invalid Python 2 exception syntax, which prevents
`chat.evals.storage` from importing. Update the `except` clause in `_git_root`
to use Python 3 multi-exception syntax with a tuple, keeping the existing
fallback behavior that returns `None` on `subprocess.CalledProcessError` or
`FileNotFoundError`. Verify the fix in the `storage.py` module so `run_evals`
and `save_run` can import successfully.

94-98: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Run-ID collisions still possible at second precision.

make_run_id still uses second-precision timestamps; two runs within the same second (same commit or both nogit) generate identical IDs, so save_run overwrites the earlier run file. This was flagged previously and remains unresolved.

🛠️ Proposed fix
 def make_run_id(git_short: str | None) -> str:
     """Generate a unique run ID based on timestamp and Git commit short."""
-    timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H-%M-%SZ")
+    timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H-%M-%S-%fZ")
     suffix = git_short or "nogit"
     return f"{timestamp}_{suffix}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 94 - 98, make_run_id still
generates run IDs at second precision, so save_run can overwrite a previous run
when two executions happen in the same second; update make_run_id to include
finer-grained uniqueness (for example, sub-second time and/or a random or
monotonic component) while keeping the existing git_short/nogit suffix, and
ensure save_run continues to use the returned ID so each run file name stays
unique.
🧹 Nitpick comments (4)
src/backend/chat/management/commands/reset_evals.py (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Constants imported by value can't be patched via chat.evals.storage.

BASELINES_DIR, DASHBOARD_DIR, and RUNS_DIR are imported directly (from chat.evals.storage import ...), binding local names in this module. Per pytest's monkeypatch guidance, tests must patch the name where it's used, i.e. chat.management.commands.reset_evals.RUNS_DIR, not chat.evals.storage.RUNS_DIR — patching the latter (as done in test_storage.py) would have no effect here. Referencing the storage module directly (storage.RUNS_DIR) instead of importing the names would avoid this footgun and keep a single source of truth.

♻️ Suggested refactor
-from chat.evals.storage import BASELINES_DIR, DASHBOARD_DIR, RUNS_DIR, reset_eval_artifacts
+from chat.evals import storage
+from chat.evals.storage import reset_eval_artifacts

Then reference storage.RUNS_DIR, storage.BASELINES_DIR, storage.DASHBOARD_DIR throughout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/reset_evals.py` at line 5, The command
module is importing BASELINES_DIR, DASHBOARD_DIR, and RUNS_DIR by value from
chat.evals.storage, which prevents tests from monkeypatching the source module
and creates a separate binding. Update reset_evals to reference the storage
module directly (for example via chat.evals.storage or an alias) and use
storage.RUNS_DIR, storage.BASELINES_DIR, and storage.DASHBOARD_DIR throughout,
keeping reset_eval_artifacts and the command logic on a single source of truth.
src/backend/chat/management/commands/run_evals.py (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove leftover commented-out debug lines.

🧹 Proposed fix
-# settings.LLM_DEFAULT_MODEL_HRID = "etalab-plateform-mistral-medium-2508"
-# settings.LANGFUSE_ENABLED = False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` around lines 20 - 21,
Remove the leftover commented-out debug settings in the run_evals command by
deleting the two inactive settings lines near the top of run_evals.py; keep the
command clean and only leave the active logic in the module.

Source: Linters/SAST tools

src/backend/chat/evals/dashboard/template.html (1)

786-789: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant nested ternary always resolves to the same value.

Every branch of kind === "regression" || kind === "partial_down" ? "row-changed" : kind === "improvement" || kind === "partial_up" ? "row-changed" : "row-changed" returns "row-changed", so this is equivalent to a single fallback value. SonarCloud also flags this (line 788: conditional returns the same value regardless of condition).

♻️ Proposed simplification
-              const rowClass = kind === "stable" ? (a.passed ? "row-pass" : "row-fail")
-                : kind === "regression" || kind === "partial_down" ? "row-changed"
-                : kind === "improvement" || kind === "partial_up" ? "row-changed" : "row-changed";
+              const rowClass = kind === "stable" ? (a.passed ? "row-pass" : "row-fail") : "row-changed";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard/template.html` around lines 786 - 789, The
rowClass ternary in template.html is redundant because every non-stable branch
returns the same "row-changed" value. Simplify the conditional around rowClass
in the dashboard template so it only checks for "stable" versus the fallback,
using the existing rowClass and caseDelta logic as the anchor points.

Source: Linters/SAST tools

src/backend/chat/tests/evals/test_production_agent.py (1)

36-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared fixtures into a conftest.py.

reset_eval_session_cache_fixture and ai_settings_fixture are duplicated verbatim in test_tool_selection.py. Moving them to a conftest.py under chat/tests/evals/ would avoid drift as more eval test modules are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_production_agent.py` around lines 36 - 66,
The two autouse fixtures in this test module are duplicated in another eval test
file, so they should be centralized. Move reset_eval_session_cache_fixture and
ai_settings_fixture into a conftest.py under chat/tests/evals/, keeping their
behavior the same and ensuring the test modules still pick them up automatically
via pytest fixture discovery.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/compare.py`:
- Around line 112-207: SonarCloud is failing because compare_runs and
format_comparison are still too cognitively complex. Refactor compare_runs by
extracting the per-dataset and per-case branching into focused helpers such as
_compare_dataset and _diff_cases, and move any repeated missing-case /
hash-mismatch handling out of the main loop. In format_comparison, finish
extracting the render logic into top-level helpers like render_case_change and
render_dataset_comparison so the main function becomes a simple orchestration
path.

In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 99-100: The partial-change badge styles in template.html use
colors in the .badge-partial-up and .badge-partial-down rules that fail minimum
contrast requirements. Update those badge color pairs to higher-contrast
foreground/background combinations while keeping the same badge classes and
preserving the existing “↑ partiel” and “↓ partiel” styling intent.
- Around line 544-584: `compareRuns` and `renderRunDetail` are too complex for
SonarCloud’s threshold; simplify them by extracting the per-dataset/per-case
processing into focused helpers. In `compareRuns`, move the row-building logic
around the datasets/cases iteration into a helper that returns the row data and
counters, and keep `compareRuns` limited to orchestration using `caseDelta`. In
`renderRunDetail`, extract the logic that renders each case row and builds the
`reasons` output into small helper functions so the main method only coordinates
the table rendering and summary assembly.

In `@src/backend/chat/management/commands/reset_evals.py`:
- Around line 46-55: _update `_print_dry_run` in `reset_evals.py` so
`reset_eval_artifacts` dry-run output matches the real behavior: `index.json`
should not be reported as a deletion. Use the existing
`RUNS_DIR`/`_print_dry_run` logic to list only files that are actually unlinked,
and represent the index reset separately as an in-place update consistent with
`src/backend/chat/evals/storage.py`’s `reset_eval_artifacts` and `_save_index`
behavior._

---

Duplicate comments:
In `@src/backend/chat/evals/storage.py`:
- Around line 41-54: The _run_git helper still uses invalid multi-exception
syntax in its except block, matching the same issue as _git_root. Update the
exception handling in _run_git to catch both subprocess.CalledProcessError and
FileNotFoundError using the correct tuple form, keeping the fallback return None
behavior unchanged. Use the _run_git function name to locate the fix.
- Around line 25-38: The `_git_root` helper still uses invalid Python 2
exception syntax, which prevents `chat.evals.storage` from importing. Update the
`except` clause in `_git_root` to use Python 3 multi-exception syntax with a
tuple, keeping the existing fallback behavior that returns `None` on
`subprocess.CalledProcessError` or `FileNotFoundError`. Verify the fix in the
`storage.py` module so `run_evals` and `save_run` can import successfully.
- Around line 94-98: make_run_id still generates run IDs at second precision, so
save_run can overwrite a previous run when two executions happen in the same
second; update make_run_id to include finer-grained uniqueness (for example,
sub-second time and/or a random or monotonic component) while keeping the
existing git_short/nogit suffix, and ensure save_run continues to use the
returned ID so each run file name stays unique.

---

Nitpick comments:
In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 786-789: The rowClass ternary in template.html is redundant
because every non-stable branch returns the same "row-changed" value. Simplify
the conditional around rowClass in the dashboard template so it only checks for
"stable" versus the fallback, using the existing rowClass and caseDelta logic as
the anchor points.

In `@src/backend/chat/management/commands/reset_evals.py`:
- Line 5: The command module is importing BASELINES_DIR, DASHBOARD_DIR, and
RUNS_DIR by value from chat.evals.storage, which prevents tests from
monkeypatching the source module and creates a separate binding. Update
reset_evals to reference the storage module directly (for example via
chat.evals.storage or an alias) and use storage.RUNS_DIR, storage.BASELINES_DIR,
and storage.DASHBOARD_DIR throughout, keeping reset_eval_artifacts and the
command logic on a single source of truth.

In `@src/backend/chat/management/commands/run_evals.py`:
- Around line 20-21: Remove the leftover commented-out debug settings in the
run_evals command by deleting the two inactive settings lines near the top of
run_evals.py; keep the command clean and only leave the active logic in the
module.

In `@src/backend/chat/tests/evals/test_production_agent.py`:
- Around line 36-66: The two autouse fixtures in this test module are duplicated
in another eval test file, so they should be centralized. Move
reset_eval_session_cache_fixture and ai_settings_fixture into a conftest.py
under chat/tests/evals/, keeping their behavior the same and ensuring the test
modules still pick them up automatically via pytest fixture discovery.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 69f63d1a-d105-4f2d-8e80-48224f3cd4a5

📥 Commits

Reviewing files that changed from the base of the PR and between a698088 and ad951fa.

📒 Files selected for processing (50)
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/self_documentation.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/conversations/settings.py
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/README.md
🚧 Files skipped from review as they are similar to previous changes (18)
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/init.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/evals/configs/self_documentation.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/evals/evaluators/init.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/evals/datasets/self_documentation.yaml
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • Makefile
  • src/backend/chat/management/commands/compare_evals.py

Comment thread src/backend/chat/evals/compare.py
Comment thread src/backend/chat/evals/dashboard/template.html Outdated
Comment thread src/backend/chat/evals/dashboard/template.html Outdated
Comment thread src/backend/chat/management/commands/reset_evals.py
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from ad951fa to 4307704 Compare July 2, 2026 10:45
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch 2 times, most recently from 9514026 to 1eea0cb Compare July 6, 2026 10:03
@suitenumerique suitenumerique deleted a comment from coderabbitai Bot Jul 6, 2026
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 1eea0cb to 8bed35e Compare July 6, 2026 10:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/backend/chat/evals/datasets/tool_selection.yaml (1)

8-631: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider YAML anchors to reduce evaluator boilerplate.

Nearly every case repeats the identical HasMatchingSpan/HasNoMatchingSpan block shape differing only in gen_ai.tool.name and evaluation_name. YAML anchors/aliases (or merge keys) could cut this duplication significantly and make future edits (e.g., adding a new tool name) less error-prone.

♻️ Example using anchors
.web_search_span: &web_search_span
  query:
    has_attributes:
      gen_ai.tool.name: "web_search"

cases:
  - name: news_recent_ai
    ...
    evaluators:
      - HasMatchingSpan:
          <<: *web_search_span
          evaluation_name: called_web_search

This is a nice-to-have; not blocking given the file is data-only and each case is still easy to read individually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/datasets/tool_selection.yaml` around lines 8 - 631,
The evaluator blocks in this YAML are highly repetitive, so refactor the
repeated HasMatchingSpan and HasNoMatchingSpan query shapes using YAML
anchors/aliases or merge keys. Define reusable anchored query templates for each
tool name in the dataset, then reference them across the cases while keeping
each case’s evaluation_name explicit; this will reduce boilerplate and make
updates in tool_selection.yaml easier to maintain.
src/backend/chat/evals/dashboard.py (2)

70-78: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Minor: direct dict indexing for run_id could raise KeyError.

entry["run_id"] is used via direct indexing in the filter (line 71) and loop (line 78) rather than .get("run_id"). If a legacy/malformed index entry lacks this key, dashboard generation will crash instead of degrading gracefully.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard.py` around lines 70 - 78, The dashboard
entry handling in `dashboard.py` uses direct `entry["run_id"]` access in both
the `kept_entries` filter and the `for entry in kept_entries` loop, which can
raise `KeyError` for malformed records. Update the logic around `kept_entries`
and `resolve_run` to read the run id safely with `.get("run_id")`, and skip or
ignore entries that do not have a valid run id so dashboard generation continues
gracefully.

36-53: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider isolating per-file YAML parse failures.

yaml.safe_load(raw) has no error handling; a single malformed or hand-edited dataset YAML file will raise and abort load_dataset_catalog entirely, breaking dashboard generation for all datasets rather than just the offending one.

♻️ Proposed fix
     for yaml_path in sorted(root.glob("*.yaml")):
         raw = yaml_path.read_text(encoding="utf-8")
-        data = yaml.safe_load(raw) or {}
+        try:
+            data = yaml.safe_load(raw) or {}
+        except yaml.YAMLError:
+            continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard.py` around lines 36 - 53,
`load_dataset_catalog` currently lets a malformed YAML file from
`yaml.safe_load` abort the whole catalog load. Wrap the per-file parse in error
handling inside `load_dataset_catalog`, and if parsing fails, skip that
yaml_path while continuing to process the remaining files. Keep the existing
behavior for valid files, and use the existing symbols `load_dataset_catalog`,
`yaml.safe_load`, and `yaml_path` to localize the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/compare.py`:
- Around line 282-363: The cognitive complexity of format_comparison is still
too high because the nested helper logic remains inside the function. Extract
_note, render_case_change, and render_dataset_comparison into top-level helpers
that return lists of rendered lines, and update format_comparison to only
orchestrate header, warnings, dataset rendering, and summary assembly using
those helpers. Use the existing symbols RunComparison, change.case_name, and
dataset_comparison to keep the extracted logic easy to locate and preserve the
current output formatting.

In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 550-552: Update caseDelta in template.html so avg_scores
comparisons are order-insensitive instead of relying on JSON.stringify key
order; normalize both b.avg_scores and a.avg_scores before comparing, or compare
their entries by key/value like Python dict equality, while keeping the existing
passed and pass_rate checks unchanged.

---

Nitpick comments:
In `@src/backend/chat/evals/dashboard.py`:
- Around line 70-78: The dashboard entry handling in `dashboard.py` uses direct
`entry["run_id"]` access in both the `kept_entries` filter and the `for entry in
kept_entries` loop, which can raise `KeyError` for malformed records. Update the
logic around `kept_entries` and `resolve_run` to read the run id safely with
`.get("run_id")`, and skip or ignore entries that do not have a valid run id so
dashboard generation continues gracefully.
- Around line 36-53: `load_dataset_catalog` currently lets a malformed YAML file
from `yaml.safe_load` abort the whole catalog load. Wrap the per-file parse in
error handling inside `load_dataset_catalog`, and if parsing fails, skip that
yaml_path while continuing to process the remaining files. Keep the existing
behavior for valid files, and use the existing symbols `load_dataset_catalog`,
`yaml.safe_load`, and `yaml_path` to localize the fix.

In `@src/backend/chat/evals/datasets/tool_selection.yaml`:
- Around line 8-631: The evaluator blocks in this YAML are highly repetitive, so
refactor the repeated HasMatchingSpan and HasNoMatchingSpan query shapes using
YAML anchors/aliases or merge keys. Define reusable anchored query templates for
each tool name in the dataset, then reference them across the cases while
keeping each case’s evaluation_name explicit; this will reduce boilerplate and
make updates in tool_selection.yaml easier to maintain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d59b1cac-3ecf-4742-bcbd-ea6831dc1922

📥 Commits

Reviewing files that changed from the base of the PR and between a698088 and 8bed35e.

📒 Files selected for processing (50)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/conversations/settings.py
✅ Files skipped from review due to trivial changes (7)
  • .codespellrc
  • src/backend/chat/evals/baselines/main.json
  • .gitignore
  • CHANGELOG.md
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/README.md
🚧 Files skipped from review as they are similar to previous changes (29)
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/evals/evaluators/init.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/conversations/settings.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • Makefile
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/evals/init.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/production_agent.py

Comment thread src/backend/chat/evals/compare.py Outdated
Comment thread src/backend/chat/evals/dashboard/template.html Outdated
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 8bed35e to 4066800 Compare July 7, 2026 07:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/backend/chat/evals/storage.py (1)

27-37: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix the exception syntax in src/backend/chat/evals/storage.py

Python 3 rejects except subprocess.CalledProcessError, FileNotFoundError: as a syntax error, so the module can’t import. Update both occurrences to tuple syntax:

Proposed fix
-    except subprocess.CalledProcessError, FileNotFoundError:
+    except (subprocess.CalledProcessError, FileNotFoundError):
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 27 - 37, The exception
handling in storage.py uses Python 2 comma syntax, which breaks import in Python
3. Update both try/except blocks around the git root lookup and any similar
handling in the same module to use tuple-based except syntax with
subprocess.CalledProcessError and FileNotFoundError, so the storage helpers can
import and run correctly.
🧹 Nitpick comments (2)
src/backend/chat/evals/dashboard/template.html (1)

730-737: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nested ternary flagged by SonarCloud.

deltaHtml/fillB use nested ternaries; not CI-blocking (warning, not failure) but worth flattening for readability, consistent with the earlier cognitive-complexity cleanup in this file.

♻️ Suggested refactor
-      const deltaHtml = pp == null
-        ? '<span class="muted">—</span>'
-        : pp === 0
-          ? '<span class="muted">=</span>'
-          : `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      let deltaHtml = '<span class="muted">—</span>';
+      if (pp !== null) {
+        deltaHtml = pp === 0
+          ? '<span class="muted">=</span>'
+          : `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard/template.html` around lines 730 - 737, The
summaryRow function contains nested ternaries in deltaHtml and fillB that should
be flattened for readability. Refactor the logic in summaryRow to use simple
if/else branches or small helper variables instead of chained ternary
expressions, keeping the same behavior for null, zero, positive, and negative
values.

Source: Linters/SAST tools

src/backend/chat/tests/evals/test_storage.py (1)

37-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding coverage for multiple named baselines.

These tests only exercise a single baseline name. Once the is_baseline flag bug in storage.py's set_baseline (clobbering other named baselines' flags) is fixed, a regression test asserting is_baseline stays True for a previously-set baseline after promoting a different one would guard against reintroduction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_storage.py` around lines 37 - 86, Add a
regression test around set_baseline in chat.evals.storage that covers promoting
multiple named baselines. Reuse the existing
test_set_baseline_copies_run_snapshot setup, then set one baseline, promote a
different baseline, and assert the first baseline’s is_baseline flag remains
True while the new one is also marked correctly. Locate the behavior in
set_baseline and verify the index.json state after each promotion to catch the
clobbering bug.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/storage.py`:
- Around line 246-251: The baseline update logic in set_baseline is overwriting
entry["is_baseline"] for every run based only on the newly promoted record,
which breaks previously named baselines. Update the storage/index refresh in
storage.py so each run’s is_baseline is derived from whether its run_id appears
in index["baselines"] values (or equivalent registry check), not just
record["run_id"]. Keep the existing index["baselines"][baseline_name]
assignment, but make the loop in set_baseline preserve any other runs already
registered as baselines.

---

Duplicate comments:
In `@src/backend/chat/evals/storage.py`:
- Around line 27-37: The exception handling in storage.py uses Python 2 comma
syntax, which breaks import in Python 3. Update both try/except blocks around
the git root lookup and any similar handling in the same module to use
tuple-based except syntax with subprocess.CalledProcessError and
FileNotFoundError, so the storage helpers can import and run correctly.

---

Nitpick comments:
In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 730-737: The summaryRow function contains nested ternaries in
deltaHtml and fillB that should be flattened for readability. Refactor the logic
in summaryRow to use simple if/else branches or small helper variables instead
of chained ternary expressions, keeping the same behavior for null, zero,
positive, and negative values.

In `@src/backend/chat/tests/evals/test_storage.py`:
- Around line 37-86: Add a regression test around set_baseline in
chat.evals.storage that covers promoting multiple named baselines. Reuse the
existing test_set_baseline_copies_run_snapshot setup, then set one baseline,
promote a different baseline, and assert the first baseline’s is_baseline flag
remains True while the new one is also marked correctly. Locate the behavior in
set_baseline and verify the index.json state after each promotion to catch the
clobbering bug.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6b83a32b-6200-41d8-a984-e0a5fc99fee5

📥 Commits

Reviewing files that changed from the base of the PR and between 8bed35e and 4066800.

📒 Files selected for processing (50)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/conversations/settings.py
✅ Files skipped from review due to trivial changes (8)
  • .codespellrc
  • src/backend/chat/evals/evaluators/init.py
  • CHANGELOG.md
  • .gitignore
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/README.md
🚧 Files skipped from review as they are similar to previous changes (30)
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/init.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/evals/configs/init.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/conversations/settings.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • Makefile
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/compare.py
  • src/backend/chat/management/commands/run_evals.py

Comment thread src/backend/chat/evals/storage.py
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch 2 times, most recently from a4138b2 to 4d1dbcc Compare July 8, 2026 09:42
Comment thread src/backend/chat/evals/README.md Outdated
Comment thread src/backend/chat/evals/README.md
Comment thread src/backend/chat/management/commands/generate_eval_dashboard.py Outdated
Comment thread src/backend/chat/evals/dashboard/template.html
Comment thread src/backend/chat/evals/datasets/faithfulness_rag.yaml
Comment thread src/backend/chat/evals/dashboard/template.html
Comment thread src/backend/chat/evals/datasets/faithfulness_rag.yaml
Comment thread src/backend/chat/evals/datasets/incertitude.yaml
Comment thread src/backend/chat/evals/datasets/tool_selection.yaml
Comment thread src/backend/chat/evals/datasets/url_hallucination.yaml
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 4d1dbcc to 46c9c0f Compare July 28, 2026 09:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/backend/chat/tests/evals/test_compare.py (1)

115-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the partial_down branch too.

partial_up and partial_down are the two halves of the same conditional in compare.py, and partial_down is the one that signals degradation. A mirrored case is nearly free here.

♻️ Suggested companion test
+def test_kind_partial_down_when_both_fail_and_rate_drops():
+    """Both runs fail but the repeat pass rate drops: kind is partial_down."""
+    partial_before = {
+        "name": "p",
+        "passed": False,
+        "pass_rate": 0.8,
+        "avg_scores": {},
+        "reasons": {},
+    }
+    partial_after = {**partial_before, "pass_rate": 0.2}
+    before = _run(run_id="before", datasets={"ds": _dataset(pass_rate=0.0, cases=[partial_before])})
+    after = _run(run_id="after", datasets={"ds": _dataset(pass_rate=0.0, cases=[partial_after])})
+
+    payload = compare_runs(before, after).to_payload()
+
+    kinds = {c["name"]: c["kind"] for c in payload["datasets"]["ds"]["case_changes"]}
+    assert kinds == {"p": "partial_down"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_compare.py` around lines 115 - 131, Add a
mirrored regression test alongside
test_kind_partial_up_when_both_fail_but_rate_improves for the partial_down
branch in compare_runs: keep both runs failing, lower the case pass_rate in the
after result, and assert the resulting case_changes entry for "p" has kind
"partial_down".
src/backend/chat/evals/dashboard/template.html (1)

716-723: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract nested ternary in summaryRow (SonarCloud flag).

deltaHtml's nested ternary is still flagged by SonarCloud. Extracting to an if/else or a small helper improves readability and clears the CI warning.

♻️ Proposed refactor
     function summaryRow(label, rateA, rateB, { isTotal = false } = {}) {
       const pp = rateA != null && rateB != null ? Math.round((rateB - rateA) * 100) : null;
-      const deltaHtml = pp == null
-        ? '<span class="muted">—</span>'
-        : pp === 0
-          ? '<span class="muted">=</span>'
-          : `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      let deltaHtml;
+      if (pp == null) {
+        deltaHtml = '<span class="muted">—</span>';
+      } else if (pp === 0) {
+        deltaHtml = '<span class="muted">=</span>';
+      } else {
+        deltaHtml = `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard/template.html` around lines 716 - 723,
Refactor the nested ternary used to build deltaHtml inside summaryRow into
explicit if/else logic or a small helper, preserving the existing null, zero,
positive, and negative output markup and classes while eliminating SonarCloud’s
nested-ternary warning.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/datasets/tool_selection.yaml`:
- Around line 908-921: Update the tool_output fixture for rapport-eval.pdf so
its summary and web_search snippets use the shared timeline: production in Q4
2025, with the March 2025 internal revision context, and remove the inconsistent
Q4 2027/T1 2028 dates while preserving the existing JSON structure.

---

Nitpick comments:
In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 716-723: Refactor the nested ternary used to build deltaHtml
inside summaryRow into explicit if/else logic or a small helper, preserving the
existing null, zero, positive, and negative output markup and classes while
eliminating SonarCloud’s nested-ternary warning.

In `@src/backend/chat/tests/evals/test_compare.py`:
- Around line 115-131: Add a mirrored regression test alongside
test_kind_partial_up_when_both_fail_but_rate_improves for the partial_down
branch in compare_runs: keep both runs failing, lower the case pass_rate in the
after result, and assert the resulting case_changes entry for "p" has kind
"partial_down".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd88ae99-790e-474f-8d13-628b9b33a0ed

📥 Commits

Reviewing files that changed from the base of the PR and between 4066800 and 46c9c0f.

📒 Files selected for processing (52)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/.pylintrc
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_configs.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/conversations/settings.py
🚧 Files skipped from review as they are similar to previous changes (29)
  • src/backend/chat/evals/evaluators/init.py
  • .codespellrc
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/init.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • .gitignore
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/configs/init.py
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/management/commands/create_eval_baseline.py
  • CHANGELOG.md
  • src/backend/conversations/settings.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/tests/test_assistant_health.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • Makefile
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/compare.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/management/commands/run_evals.py

Comment thread src/backend/chat/evals/datasets/tool_selection.yaml
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch 4 times, most recently from 07c200b to 205e2bd Compare July 28, 2026 14:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/backend/chat/evals/storage.py (1)

28-37: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use Python 3 exception tuple syntax.

storage.py is still parsed as Python 3, so except subprocess.CalledProcessError, FileNotFoundError: is invalid syntax at both lines 36 and 52 and prevents the module from importing.

🐛 Proposed fix (apply to both lines 36 and 52)
-    except subprocess.CalledProcessError, FileNotFoundError:
+    except (subprocess.CalledProcessError, FileNotFoundError):
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 28 - 37, Update both
exception handlers in the storage path-resolution logic to use Python 3 tuple
syntax, replacing the comma-separated exception form in the relevant try/except
blocks while preserving the existing return None behavior.
🧹 Nitpick comments (3)
src/backend/chat/tests/agents/test_albert_models.py (1)

283-310: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the non-dict tool_call defensive branch.

_validate_completion guards against tool_call entries that aren't dicts (if not isinstance(tool_call, dict): continue in albert_models.py), but no test here exercises that path. Consider a case where choices[0].message.tool_calls contains a non-dict entry to lock in that defensive behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/agents/test_albert_models.py` around lines 283 - 310,
Add a focused test alongside the existing _validate_completion tests that builds
a completion whose choices[0].message.tool_calls contains a non-dict entry, then
calls albert_model._validate_completion and verifies validation completes
without error while preserving or skipping that entry according to the existing
defensive behavior. Reuse _make_malformed_chat_completion and the established
completion/message fixtures or symbols rather than adding unrelated coverage.
src/backend/chat/evals/dashboard/template.html (1)

76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Low-contrast header subtitle text.

.site-header-subtitle uses #94a3b8 on the dark .site-header gradient background, flagged by SonarCloud as failing minimum contrast.

💡 Proposed fix
     .site-header-subtitle {
       margin: .45rem 0 0;
       max-width: 36rem;
       font-size: .92rem;
       line-height: 1.45;
-      color: `#94a3b8`;
+      color: `#cbd5e1`;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard/template.html` at line 76, Update the
.site-header-subtitle color from `#94a3b8` to a lighter color that meets
SonarCloud’s minimum contrast requirement against the dark .site-header
gradient, while preserving the existing header styling.

Source: Linters/SAST tools

src/backend/chat/providers/albert_models.py (1)

78-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the tool-call normalization loop to satisfy the SonarCloud complexity gate.

Cognitive complexity is 16 vs the 15 allowed (CI gate failure). Pulling the inner for tool_call in ... loop into a small helper keeps _validate_completion as orchestration only.

♻️ Proposed refactor
+    `@staticmethod`
+    def _normalize_tool_call_types(choices: list[dict]) -> None:
+        for choice in choices:
+            for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
+                if not isinstance(tool_call, dict):
+                    continue
+                is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
+                if not is_custom and tool_call.get("type") != "function":
+                    tool_call["type"] = "function"
+
     def _validate_completion(self, response: chat.ChatCompletion) -> _ChatCompletion:
         ...
         if not isinstance(data.get("choices"), list):
             data["choices"] = []
-
-        for choice in data.get("choices") or []:
-            for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
-                if not isinstance(tool_call, dict):
-                    continue
-                is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
-                if not is_custom and tool_call.get("type") != "function":
-                    tool_call["type"] = "function"
-
+        self._normalize_tool_call_types(data["choices"])
         return _ChatCompletion.model_validate(data)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/providers/albert_models.py` around lines 78 - 108, Extract
the inner tool-call normalization loop from _validate_completion into a small
dedicated helper that accepts the relevant message or tool-call collection and
preserves the existing custom/function type rules. Have _validate_completion
delegate to this helper while retaining only response normalization and
orchestration, reducing its cognitive complexity without changing behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 397: Move the “add run_evals management command” changelog entry from the
version 0.0.1 section into the existing “## [unreleased]” section, preserving
its text and ensuring the existing Line 399 reference definition remains used.

In `@src/backend/chat/management/commands/run_evals.py`:
- Around line 86-91: Extend the validation around the save option in the
run-evals command so --save cannot be combined with either --case or --dataset.
Preserve the existing CommandError behavior and message context, updating it to
identify both filters as unsupported for saved partial runs.
- Line 106: Update the argument validation in the eval command before the
REGISTRY selection so providing --case without --dataset raises a clear
CommandError. Keep the existing configs selection unchanged when --dataset is
supplied or when running without a case filter.

---

Duplicate comments:
In `@src/backend/chat/evals/storage.py`:
- Around line 28-37: Update both exception handlers in the storage
path-resolution logic to use Python 3 tuple syntax, replacing the
comma-separated exception form in the relevant try/except blocks while
preserving the existing return None behavior.

---

Nitpick comments:
In `@src/backend/chat/evals/dashboard/template.html`:
- Line 76: Update the .site-header-subtitle color from `#94a3b8` to a lighter
color that meets SonarCloud’s minimum contrast requirement against the dark
.site-header gradient, while preserving the existing header styling.

In `@src/backend/chat/providers/albert_models.py`:
- Around line 78-108: Extract the inner tool-call normalization loop from
_validate_completion into a small dedicated helper that accepts the relevant
message or tool-call collection and preserves the existing custom/function type
rules. Have _validate_completion delegate to this helper while retaining only
response normalization and orchestration, reducing its cognitive complexity
without changing behavior.

In `@src/backend/chat/tests/agents/test_albert_models.py`:
- Around line 283-310: Add a focused test alongside the existing
_validate_completion tests that builds a completion whose
choices[0].message.tool_calls contains a non-dict entry, then calls
albert_model._validate_completion and verifies validation completes without
error while preserving or skipping that entry according to the existing
defensive behavior. Reuse _make_malformed_chat_completion and the established
completion/message fixtures or symbols rather than adding unrelated coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bb2209f-7a0a-4cc5-b30b-4d0be810bcd8

📥 Commits

Reviewing files that changed from the base of the PR and between 46c9c0f and 205e2bd.

⛔ Files ignored due to path filters (1)
  • src/backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/.pylintrc
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_configs.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/conversations/settings.py
  • src/backend/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (33)
  • src/backend/chat/evals/init.py
  • .codespellrc
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/configs/init.py
  • src/backend/.pylintrc
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/configs/url_hallucination.py
  • .gitignore
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_configs.py
  • src/backend/chat/evals/evaluators/init.py
  • src/backend/chat/evals/tool_output.py
  • Makefile
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/compare.py

Comment thread CHANGELOG.md
Comment on lines +86 to +91
if options["save"] and options["case"]:
raise CommandError(
"--save cannot be combined with --case: a partial run would register "
"every other case as a coverage gap (= regression) when compared "
"against the baseline."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject dataset-filtered saved runs too.

--dataset also creates a partial record: Lines 106 and 121-128 persist only that dataset, so comparison against a full baseline will classify every omitted dataset as a coverage regression. Apply the same --save restriction to --dataset, or make comparison explicitly support scoped baselines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` around lines 86 - 91,
Extend the validation around the save option in the run-evals command so --save
cannot be combined with either --case or --dataset. Preserve the existing
CommandError behavior and message context, updating it to identify both filters
as unsupported for saved partial runs.

use_llm_judge = not options["no_llm_judge"]
self._configure_judge(use_llm_judge)

configs = [REGISTRY[options["dataset"]]] if options["dataset"] else list(REGISTRY.values())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require --dataset when filtering by --case.

The selected case is passed to every registered dataset. If it exists in only one dataset, _load_dataset aborts on the first other dataset instead of running the requested case. Reject --case without --dataset with a clear CommandError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` at line 106, Update the
argument validation in the eval command before the REGISTRY selection so
providing --case without --dataset raises a clear CommandError. Keep the
existing configs selection unchanged when --dataset is supplied or when running
without a case filter.

@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 205e2bd to 18e7ee7 Compare July 30, 2026 12:40
- run_evals management command and Make targets
- datasets, evaluators, save/compare/baseline flow
- HTML dashboard
@camilleAND
camilleAND force-pushed the maxenceh/setup-eval-llm branch from 18e7ee7 to f63a865 Compare July 30, 2026 12:45
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (5)
src/backend/chat/evals/storage.py (2)

36-37: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Python 2 except syntax — module fails to import.

except subprocess.CalledProcessError, FileNotFoundError: is a hard SyntaxError in Python 3, so storage.py (and every command importing it) cannot load. Same at Line 52. This was previously reported as fixed, but the supplied code still has it.

🐛 Proposed fix (apply to both lines 36 and 52)
-    except subprocess.CalledProcessError, FileNotFoundError:
+    except (subprocess.CalledProcessError, FileNotFoundError):
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 36 - 37, Update both
exception handlers in storage.py to use Python 3 tuple syntax for catching
subprocess.CalledProcessError and FileNotFoundError, preserving their existing
return behavior so the module imports successfully.

93-97: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Run IDs still collide at second precision.

Two runs in the same second on the same commit produce the same run_id, so save_run overwrites the earlier artifact and the index entry is replaced. Add sub-second precision or a short random suffix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/storage.py` around lines 93 - 97, Update make_run_id
so generated IDs remain unique for runs sharing the same second and Git commit,
by adding sub-second timestamp precision or a short random suffix while
preserving the existing timestamp and git_short components.
src/backend/chat/management/commands/reset_evals.py (1)

49-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Dry-run still reports index.json as deleted.

reset_eval_artifacts excludes index.json from unlinking (storage.py Line 313) and only clears its contents in place, and Line 55 also appends it even when it doesn't exist.

🩹 Proposed fix
-            targets.append(str((RUNS_DIR / "index.json").relative_to(RUNS_DIR.parent)))
+            index_path = RUNS_DIR / "index.json"
+            if index_path.exists():
+                targets.append(f"{index_path.relative_to(RUNS_DIR.parent)} (reset, not deleted)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/reset_evals.py` around lines 49 - 55,
Update the target collection in reset_eval_artifacts so dry-run output matches
the actual cleanup behavior: exclude index.json from the deletion targets and
only include it when it exists and is being cleared in place. Preserve reporting
of other matching run JSON files.
src/backend/chat/management/commands/run_evals.py (2)

86-91: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Incomplete flag-combination validation in handle. Only --save + --case is rejected; the other two invalid combinations involving --dataset slip through and either persist a partial run or abort a legitimate single-case run.

  • src/backend/chat/management/commands/run_evals.py#L86-L91: extend the guard to also reject --save with --dataset (partial record → every omitted dataset scores as a coverage gap against a full baseline), and to reject --case without --dataset.
  • src/backend/chat/management/commands/run_evals.py#L106: with that guard in place, configs is a single dataset whenever --case is set, so _load_dataset no longer aborts on an unrelated dataset.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` around lines 86 - 91, The
handle validation guard must reject all invalid combinations: --save with
--case, --save with --dataset, and --case without --dataset. Update the guard
around the existing CommandError, and ensure the configs logic near line 106
uses only the single dataset when --case is set so _load_dataset does not
process unrelated datasets.

106-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

--case without --dataset aborts instead of running the case.

The case name is applied to every registered dataset, and _load_dataset raises CommandError for the first dataset that lacks it. Require --dataset when --case is given.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/run_evals.py` at line 106, Update the
option validation in the run-evals command before the configs assignment to
reject --case unless --dataset is also provided, raising the command’s existing
CommandError with a clear message. Preserve the current behavior for
dataset-only and case-plus-dataset invocations, and prevent the case from being
applied across all REGISTRY entries.
🧹 Nitpick comments (8)
src/backend/chat/evals/dashboard/template.html (1)

716-731: 📐 Maintainability & Code Quality | 🔵 Trivial

Nested ternaries in summaryRow/deltaHtml.

SonarCloud flags this construct for readability; consider extracting to an if/else chain or small helper for clarity.

♻️ Suggested refactor
-      const deltaHtml = pp == null
-        ? '<span class="muted">—</span>'
-        : pp === 0
-          ? '<span class="muted">=</span>'
-          : `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      let deltaHtml = '<span class="muted">—</span>';
+      if (pp === 0) {
+        deltaHtml = '<span class="muted">=</span>';
+      } else if (pp != null) {
+        deltaHtml = `<span class="${pp > 0 ? "delta-pos" : "delta-neg"}">${pp > 0 ? "+" : ""}${pp}pp</span>`;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/dashboard/template.html` around lines 716 - 731,
Refactor the nested ternary expressions in summaryRow, especially the deltaHtml
and fillB assignments, into clear if/else logic or a small local helper.
Preserve the existing null, zero, positive, and negative rate-difference outputs
and class assignments without changing behavior.

Source: Linters/SAST tools

.codespellrc (1)

14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider narrowing the skip to the data directories.

The whole chat/evals package is excluded, so Python sources and docstrings there lose spellchecking too. Skipping only ./src/backend/chat/evals/datasets and ./src/backend/chat/evals/baselines keeps coverage on code while ignoring the French fixtures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.codespellrc at line 14, Update the codespell exclusions to remove the broad
./src/backend/chat/evals entry and instead skip only
./src/backend/chat/evals/datasets and ./src/backend/chat/evals/baselines,
preserving spellchecking for the package’s Python code and docstrings.
src/backend/.pylintrc (1)

22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why parallel Pylint was disabled.

Forcing jobs=1 slows lint across the whole backend, and the change is unrelated to the eval feature. If this works around a parallel-mode issue (e.g. pylint_django/similarity checker instability with the new modules), add an inline comment; otherwise consider reverting to jobs=0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/.pylintrc` at line 22, Update the jobs setting in the Pylint
configuration to use parallel execution unless jobs=1 is required for a known
issue; if retaining jobs=1, add an inline comment documenting the specific
parallel-mode failure or dependency instability it avoids.
src/backend/chat/evals/evaluators/span.py (1)

21-22: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard against missing span-tree instrumentation.

ctx.span_tree raises SpanTreeRecordingError when the tree was not captured, so a single uninstrumented case turns each HasNoMatchingSpan did_not_call_* assertion into an evaluator error instead of a pass/fail result. Catch that exception and return a failed span-negative result before calling .any(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/evaluators/span.py` around lines 21 - 22, Update
HasNoMatchingSpan.evaluate to catch SpanTreeRecordingError when accessing
ctx.span_tree and return False before invoking any(self.query). Preserve the
existing negated any(...) behavior when span-tree instrumentation is available.
src/backend/chat/tests/evals/test_configs.py (1)

40-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test duplicates run_evals.py's dataset-loading logic instead of exercising it directly.

This re-implements _load_dataset's custom_evaluator_types assembly and Dataset.from_text call rather than calling shared logic, so the test can stay green even if the actual run_evals path diverges — undermining its own stated purpose of validating "the run_evals path".

Consider extracting the loading logic (e.g. EvalConfig.build_dataset(case_name=None) in configs/base.py) and having both run_evals.py and this test call it, so a regression in the real loading path is guaranteed to fail here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_configs.py` around lines 40 - 54, Extract
the shared dataset-construction logic from run_evals.py into an EvalConfig
method such as build_dataset(case_name=None) in configs/base.py, including
evaluator-type assembly and Dataset.from_text setup. Update run_evals.py and
test_all_datasets_parse_after_config_strip to call this method, while preserving
each caller’s existing name and case-selection behavior.
src/backend/chat/evals/production_agent.py (1)

102-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating eval-vs-production wiring to reduce drift risk.

build_production_agent_service reimplements the web_search_enabled default logic (_is_web_search_enabled and _is_smart_search_enabled), and ensure_web_search_registered reimplements its own only_if_web_search_enabled prepare-gate and tool registration — both mirroring behavior that appears to live in AIAgentService._setup_web_search_tool/_run_agent. Since the whole point of this module is to test the real production wiring, any future change to that upstream logic won't automatically propagate here, silently making evals test stale behavior.

Consider exposing a small shared helper (e.g., a method or module-level function used by both AIAgentService and this eval harness) for the enablement decision and the stub-registration prepare-gate, so eval wiring can't drift from production wiring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/production_agent.py` around lines 102 - 174,
Consolidate web-search enablement and registration logic shared by
AIAgentService and the eval harness into reusable helpers, then update
build_production_agent_service and ensure_web_search_registered to call them
instead of duplicating the _is_web_search_enabled/_is_smart_search_enabled
decision and only_if_web_search_enabled gate/tool setup. Keep production and
eval behavior identical while ensuring future changes to the shared wiring
propagate automatically.
src/backend/chat/evals/tool_stub_responses.py (1)

79-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider rejecting unknown keys in dataset tool_output JSON.

ToolStubResponses uses pydantic's default extra="ignore". A typo in a dataset's tool_output JSON key (e.g. misspelling web_search) would silently fall back to defaults instead of failing, making dataset authoring mistakes hard to catch.

♻️ Proposed fix
-class ToolStubResponses(BaseModel):
+class ToolStubResponses(BaseModel):
     """Per-case simulated tool payloads (JSON in dataset ``tool_output``)."""
+
+    model_config = ConfigDict(extra="forbid")
 
     web_search: dict[str, Any] | None = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/tool_stub_responses.py` around lines 79 - 86,
Configure the ToolStubResponses Pydantic model to reject unknown fields in
dataset tool_output JSON instead of silently ignoring them. Update the model
configuration near the existing fields while preserving all declared fields and
their defaults.
src/backend/chat/management/commands/create_eval_baseline.py (1)

30-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant resolve_run call.

set_baseline already calls resolve_run internally and raises the same FileNotFoundError, so Line 32 just re-reads and re-parses the run file.

♻️ Proposed cleanup
         try:
-            resolve_run(options["run"])
             baseline = set_baseline(

(drop resolve_run from the import as well)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/create_eval_baseline.py` around lines 30
- 39, Remove the redundant resolve_run(options["run"]) invocation from handle
and drop resolve_run from the imports. Keep set_baseline as the sole
run-resolution path while preserving the existing
FileNotFoundError-to-CommandError handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/chat/evals/baselines/main_run.json`:
- Around line 5-10: Regenerate the baseline metadata in main_run.json from a
clean checkout of the finalized merge commit, ensuring the git dirty flag is
false and the recorded commit, short commit, and branch reference the
reproducible merge state rather than the dirty feature-branch working tree.

In `@src/backend/chat/evals/compare.py`:
- Around line 139-271: Update _compare_case to create a CaseChange when a case
exists only in after_cases, instead of returning None. Add a _new_case_change
helper mirroring _missing_after_case_change, mark the result as a coverage gap
with a distinct "new_case" kind, and preserve the after-case metrics and
reasons. Update template.html’s comparisonRows() to iterate the union of before
and after case names so new cases appear in reports.

In `@src/backend/chat/evals/configs/faithfulness_rag.py`:
- Around line 71-87: Move the production_agent_deps(service) call and
web_search_enabled assignment from make_faithfulness_rag_task_fn into run_agent
so each eval case receives fresh per-call dependencies. Keep the agent and
tool-stub setup unchanged, and ensure run_agent passes the newly created deps to
agent.run.

In `@src/backend/chat/evals/datasets/tool_selection.yaml`:
- Around line 111-123: Update the web_search tool_output fixture in the dataset
entry to remove the stale François Bayrou officeholder claim and use a
timeline-neutral placeholder consistent with the mid-2026 framing. Preserve the
existing JSON structure, URL, title, and span-oriented evaluation behavior while
avoiding a specific current Prime Minister or date that requires future factual
updates.

In `@src/backend/chat/evals/evaluators/url_regex.py`:
- Around line 23-24: Update evaluate in the URL regex evaluator to validate
ctx.output with an isinstance(..., str) guard before calling _extract_urls,
matching the handling of other inputs. Ensure non-string output follows the
evaluator’s existing non-match/reasoning path instead of reaching re.findall and
raising TypeError.

In `@src/backend/chat/evals/production_agent.py`:
- Around line 71-99: Reset or recreate the eval conversation before each eval
case/run so _get_eval_session_conversation does not reuse prior
pydantic_messages; update the surrounding eval setup or run flow while
preserving the shared eval user behavior. Also synchronize the module-global
initialization in _get_eval_session_user and _get_eval_session_conversation,
including their get_or_create paths, so parallel execution cannot duplicate
creation.

In `@src/backend/chat/management/commands/generate_eval_dashboard.py`:
- Around line 16-25: Update the path formatting in handle around
output_path.relative_to(settings.BASE_DIR) to catch cases where output_path is
outside settings.BASE_DIR, preserving the successful dashboard generation and
falling back to displaying output_path’s absolute path. Keep the existing
host-side display path when relative_to succeeds.

In `@src/backend/chat/providers/albert_models.py`:
- Around line 78-108: Reduce cognitive complexity in _validate_completion by
extracting the tool-call type normalization loop and its custom-payload
condition into a small helper. Have _validate_completion invoke the helper for
each valid choice while preserving the existing handling of malformed choices,
tool calls, and genuine custom tool calls.

In `@src/backend/chat/tests/evals/test_compare.py`:
- Around line 47-49: Update the comparison fixture and assertions around
comparison.coverage_gaps and comparison.regressions so cases missing from the
after run are represented with after_passed unknown and excluded from
regressions. Preserve coverage_gaps for those cases, and adjust the expected
regression count to verify previously passing missing cases are not
double-counted.

In `@src/backend/conversations/settings.py`:
- Around line 745-748: Remove the hardcoded “Mistral Medium 2508” identity from
the default prompt in the assistant instructions, while preserving the
deployment-neutral description and the existing prohibition against presenting
assumptions as facts. Ensure model-specific identity is obtained from the
resolved LLM configuration or the self_documentation tool so the about_model
path reflects the active deployment.

---

Duplicate comments:
In `@src/backend/chat/evals/storage.py`:
- Around line 36-37: Update both exception handlers in storage.py to use Python
3 tuple syntax for catching subprocess.CalledProcessError and FileNotFoundError,
preserving their existing return behavior so the module imports successfully.
- Around line 93-97: Update make_run_id so generated IDs remain unique for runs
sharing the same second and Git commit, by adding sub-second timestamp precision
or a short random suffix while preserving the existing timestamp and git_short
components.

In `@src/backend/chat/management/commands/reset_evals.py`:
- Around line 49-55: Update the target collection in reset_eval_artifacts so
dry-run output matches the actual cleanup behavior: exclude index.json from the
deletion targets and only include it when it exists and is being cleared in
place. Preserve reporting of other matching run JSON files.

In `@src/backend/chat/management/commands/run_evals.py`:
- Around line 86-91: The handle validation guard must reject all invalid
combinations: --save with --case, --save with --dataset, and --case without
--dataset. Update the guard around the existing CommandError, and ensure the
configs logic near line 106 uses only the single dataset when --case is set so
_load_dataset does not process unrelated datasets.
- Line 106: Update the option validation in the run-evals command before the
configs assignment to reject --case unless --dataset is also provided, raising
the command’s existing CommandError with a clear message. Preserve the current
behavior for dataset-only and case-plus-dataset invocations, and prevent the
case from being applied across all REGISTRY entries.

---

Nitpick comments:
In @.codespellrc:
- Line 14: Update the codespell exclusions to remove the broad
./src/backend/chat/evals entry and instead skip only
./src/backend/chat/evals/datasets and ./src/backend/chat/evals/baselines,
preserving spellchecking for the package’s Python code and docstrings.

In `@src/backend/.pylintrc`:
- Line 22: Update the jobs setting in the Pylint configuration to use parallel
execution unless jobs=1 is required for a known issue; if retaining jobs=1, add
an inline comment documenting the specific parallel-mode failure or dependency
instability it avoids.

In `@src/backend/chat/evals/dashboard/template.html`:
- Around line 716-731: Refactor the nested ternary expressions in summaryRow,
especially the deltaHtml and fillB assignments, into clear if/else logic or a
small local helper. Preserve the existing null, zero, positive, and negative
rate-difference outputs and class assignments without changing behavior.

In `@src/backend/chat/evals/evaluators/span.py`:
- Around line 21-22: Update HasNoMatchingSpan.evaluate to catch
SpanTreeRecordingError when accessing ctx.span_tree and return False before
invoking any(self.query). Preserve the existing negated any(...) behavior when
span-tree instrumentation is available.

In `@src/backend/chat/evals/production_agent.py`:
- Around line 102-174: Consolidate web-search enablement and registration logic
shared by AIAgentService and the eval harness into reusable helpers, then update
build_production_agent_service and ensure_web_search_registered to call them
instead of duplicating the _is_web_search_enabled/_is_smart_search_enabled
decision and only_if_web_search_enabled gate/tool setup. Keep production and
eval behavior identical while ensuring future changes to the shared wiring
propagate automatically.

In `@src/backend/chat/evals/tool_stub_responses.py`:
- Around line 79-86: Configure the ToolStubResponses Pydantic model to reject
unknown fields in dataset tool_output JSON instead of silently ignoring them.
Update the model configuration near the existing fields while preserving all
declared fields and their defaults.

In `@src/backend/chat/management/commands/create_eval_baseline.py`:
- Around line 30-39: Remove the redundant resolve_run(options["run"]) invocation
from handle and drop resolve_run from the imports. Keep set_baseline as the sole
run-resolution path while preserving the existing
FileNotFoundError-to-CommandError handling.

In `@src/backend/chat/tests/evals/test_configs.py`:
- Around line 40-54: Extract the shared dataset-construction logic from
run_evals.py into an EvalConfig method such as build_dataset(case_name=None) in
configs/base.py, including evaluator-type assembly and Dataset.from_text setup.
Update run_evals.py and test_all_datasets_parse_after_config_strip to call this
method, while preserving each caller’s existing name and case-selection
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f8e3555-16ea-47ec-b837-9a97078d3477

📥 Commits

Reviewing files that changed from the base of the PR and between 205e2bd and 18e7ee7.

⛔ Files ignored due to path filters (1)
  • src/backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (54)
  • .codespellrc
  • .gitignore
  • CHANGELOG.md
  • Makefile
  • src/backend/.pylintrc
  • src/backend/chat/agents/base.py
  • src/backend/chat/evals/README.md
  • src/backend/chat/evals/__init__.py
  • src/backend/chat/evals/baselines/.gitkeep
  • src/backend/chat/evals/baselines/main.json
  • src/backend/chat/evals/baselines/main_run.json
  • src/backend/chat/evals/compare.py
  • src/backend/chat/evals/configs/__init__.py
  • src/backend/chat/evals/configs/base.py
  • src/backend/chat/evals/configs/faithfulness_rag.py
  • src/backend/chat/evals/configs/incertitude.py
  • src/backend/chat/evals/configs/tool_selection.py
  • src/backend/chat/evals/configs/url_hallucination.py
  • src/backend/chat/evals/dashboard.py
  • src/backend/chat/evals/dashboard/.gitkeep
  • src/backend/chat/evals/dashboard/template.html
  • src/backend/chat/evals/datasets/faithfulness_rag.yaml
  • src/backend/chat/evals/datasets/incertitude.yaml
  • src/backend/chat/evals/datasets/tool_selection.yaml
  • src/backend/chat/evals/datasets/url_hallucination.yaml
  • src/backend/chat/evals/evaluators/__init__.py
  • src/backend/chat/evals/evaluators/span.py
  • src/backend/chat/evals/evaluators/url_regex.py
  • src/backend/chat/evals/production_agent.py
  • src/backend/chat/evals/report_builder.py
  • src/backend/chat/evals/runs/.gitkeep
  • src/backend/chat/evals/storage.py
  • src/backend/chat/evals/tool_output.py
  • src/backend/chat/evals/tool_stub_responses.py
  • src/backend/chat/management/commands/compare_evals.py
  • src/backend/chat/management/commands/create_eval_baseline.py
  • src/backend/chat/management/commands/generate_eval_dashboard.py
  • src/backend/chat/management/commands/reset_evals.py
  • src/backend/chat/management/commands/run_evals.py
  • src/backend/chat/providers/albert_models.py
  • src/backend/chat/tests/agents/test_albert_models.py
  • src/backend/chat/tests/evals/__init__.py
  • src/backend/chat/tests/evals/test_compare.py
  • src/backend/chat/tests/evals/test_configs.py
  • src/backend/chat/tests/evals/test_dashboard.py
  • src/backend/chat/tests/evals/test_production_agent.py
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/tests/evals/test_reset_evals.py
  • src/backend/chat/tests/evals/test_storage.py
  • src/backend/chat/tests/evals/test_tool_selection.py
  • src/backend/chat/tests/evals/test_tool_stub_responses.py
  • src/backend/chat/tools/descriptions.py
  • src/backend/conversations/settings.py
  • src/backend/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (5)
  • CHANGELOG.md
  • src/backend/chat/evals/configs/init.py
  • .gitignore
  • src/backend/chat/tests/evals/test_report_builder.py
  • src/backend/chat/evals/README.md

Comment on lines +5 to +10
"git": {
"commit": "205e2bde3163636e9db9fa288e7f016e3fe43975",
"commit_short": "205e2bd",
"branch": "maxenceh/setup-eval-llm",
"dirty": true
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Baseline captured from a dirty working tree.

"dirty": true on a feature branch means this committed reference can't be reproduced from 205e2bd, which weakens future regression comparisons. Consider regenerating the baseline from a clean checkout of the merge commit once this lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/baselines/main_run.json` around lines 5 - 10,
Regenerate the baseline metadata in main_run.json from a clean checkout of the
finalized merge commit, ensuring the git dirty flag is false and the recorded
commit, short commit, and branch reference the reproducible merge state rather
than the dirty feature-branch working tree.

Comment on lines +139 to +271
def _missing_after_case_change(
*,
dataset_name: str,
before_case: dict[str, Any],
) -> CaseChange:
"""Build a CaseChange for a case that is absent from the after run."""
return CaseChange(
dataset=dataset_name,
case_name=before_case["name"],
before_passed=before_case["passed"],
after_passed=False,
before_pass_rate=before_case["pass_rate"],
after_pass_rate=0.0,
before_avg_scores=before_case["avg_scores"],
after_avg_scores={},
reasons={"_coverage": "missing from after run"},
coverage_gap=True,
)


def _dataset_comparison_missing_after(
dataset_name: str,
before_dataset: dict[str, Any],
) -> DatasetComparison:
"""Build a dataset comparison when the after run has no results for it."""
case_changes = [
_missing_after_case_change(dataset_name=dataset_name, before_case=before_case)
for before_case in sorted(before_dataset.get("cases", []), key=lambda case: case["name"])
]
return DatasetComparison(
dataset=dataset_name,
before_pass_rate=before_dataset["pass_rate"],
after_pass_rate=0.0,
before_pass_rate_avg_repeats=before_dataset["pass_rate_avg_repeats"],
after_pass_rate_avg_repeats=0.0,
dataset_hash_match=False,
case_changes=case_changes,
)


def _cases_unchanged(
before_case: dict[str, Any],
after_case: dict[str, Any],
) -> bool:
"""Return whether pass status, pass rate, and scores are identical."""
return (
before_case["passed"] == after_case["passed"]
and before_case["pass_rate"] == after_case["pass_rate"]
and before_case["avg_scores"] == after_case["avg_scores"]
)


def _case_change_for_pair(
*,
dataset_name: str,
case_name: str,
before_case: dict[str, Any],
after_case: dict[str, Any],
) -> CaseChange:
"""Build a CaseChange for a case present in both runs with differing results."""
return CaseChange(
dataset=dataset_name,
case_name=case_name,
before_passed=before_case["passed"],
after_passed=after_case["passed"],
before_pass_rate=before_case["pass_rate"],
after_pass_rate=after_case["pass_rate"],
before_avg_scores=before_case["avg_scores"],
after_avg_scores=after_case["avg_scores"],
reasons=after_case.get("reasons", {}),
)


def _compare_case(
*,
dataset_name: str,
case_name: str,
before_cases: dict[str, dict[str, Any]],
after_cases: dict[str, dict[str, Any]],
) -> CaseChange | None:
"""Compare one case across runs; return a CaseChange when a delta exists."""
before_case = before_cases.get(case_name)
after_case = after_cases.get(case_name)
if after_case is None:
if before_case is not None:
return _missing_after_case_change(
dataset_name=dataset_name,
before_case=before_case,
)
return None
if before_case is None or _cases_unchanged(before_case, after_case):
return None
return _case_change_for_pair(
dataset_name=dataset_name,
case_name=case_name,
before_case=before_case,
after_case=after_case,
)


def _compare_datasets(
*,
dataset_name: str,
before_dataset: dict[str, Any],
after_dataset: dict[str, Any],
dataset_hash_match: bool,
) -> DatasetComparison:
"""Compare one dataset present in both runs."""
before_cases = _case_map(before_dataset)
after_cases = _case_map(after_dataset)
case_changes = [
change
for case_name in sorted(set(before_cases) | set(after_cases))
if (
change := _compare_case(
dataset_name=dataset_name,
case_name=case_name,
before_cases=before_cases,
after_cases=after_cases,
)
)
is not None
]

return DatasetComparison(
dataset=dataset_name,
before_pass_rate=before_dataset["pass_rate"],
after_pass_rate=after_dataset["pass_rate"],
before_pass_rate_avg_repeats=before_dataset["pass_rate_avg_repeats"],
after_pass_rate_avg_repeats=after_dataset["pass_rate_avg_repeats"],
dataset_hash_match=dataset_hash_match,
case_changes=case_changes,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

New cases (only in the after run) are silently dropped from the comparison.

_compare_case handles the "missing from after" direction explicitly (_missing_after_case_change creates a coverage_gap CaseChange), but the symmetric case — a case that exists only in after_case because before_case is None — hits if before_case is None or _cases_unchanged(...): return None and produces no CaseChange at all. New cases added to a dataset therefore never show up in case_changes, regressions, improvements, or coverage_gaps, even if the new case fails. The dataset's aggregate pass_rate/cases_total still reflect it, and the raw run-detail view still lists it, but the diff/regression report gives no indication a case is new or why the numbers shifted.

Since template.html's comparisonRows() also only iterates bd.cases (the before dataset), fully surfacing this would additionally require unioning case names there — worth keeping in mind if this is addressed.

💡 Illustrative fix direction
-    if before_case is None or _cases_unchanged(before_case, after_case):
+    if before_case is None:
+        return _new_case_change(dataset_name=dataset_name, after_case=after_case)
+    if _cases_unchanged(before_case, after_case):
         return None

(with a _new_case_change helper mirroring _missing_after_case_change, and a "new_case" branch added to CaseChange.kind.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/compare.py` around lines 139 - 271, Update
_compare_case to create a CaseChange when a case exists only in after_cases,
instead of returning None. Add a _new_case_change helper mirroring
_missing_after_case_change, mark the result as a coverage gap with a distinct
"new_case" kind, and preserve the after-case metrics and reasons. Update
template.html’s comparisonRows() to iterate the union of before and after case
names so new cases appear in reports.

Comment on lines +71 to +87
def make_faithfulness_rag_task_fn(model_hrid: str):
"""Build the task function: production wiring + stub RAG implementation."""
service = _build_faithfulness_rag_service(model_hrid)
stub_document_search_rag(service, _stub_document_search_rag)
agent = service.conversation_agent
deps = production_agent_deps(service)
deps.web_search_enabled = False

async def run_agent(inputs: EvalInputs) -> str:
stubs = ToolStubResponses(document_search_rag=inputs.tool_output or _NO_PASSAGES)
token = set_current_tool_stubs(stubs)
try:
return (await agent.run(inputs.user_message, deps=deps)).output
finally:
reset_current_tool_stubs(token)

return run_agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'faithfulness_rag\.py|tool_selection\.py|production_agent\.py|conversation_agent' || true

echo
echo "faithfulness_rag outline:"
ast-grep outline src/backend/chat/evals/configs/faithfulness_rag.py --view expanded || true

echo
echo "faithfulness_rag relevant lines:"
sed -n '1,140p' src/backend/chat/evals/configs/faithfulness_rag.py | cat -n

echo
echo "tool_selection relevant patterns:"
fd -i 'tool_selection\.py' . | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  rg -n "production_agent_deps|make_.*task_fn|run_agent|web_search_enabled" "$f" -C 3
done

Repository: suitenumerique/conversations

Length of output: 6788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search production_agent_deps definitions/usages:"
rg -n "def production_agent_deps|production_agent_deps\(|class ProductionAgentDeps|ProductionAgentDeps|conversation_agent\.run|def run\(" src -g '*.py'

echo
echo "Candidate files:"
git ls-files '*.py' | rg 'chat/|evals/|agent|deps' | head -200

echo
echo "Inspect source snippets from files containing production_agent_deps:"
rg -l "production_agent_deps" src/backend/chat evals 2>/dev/null || true
for f in $(rg -l "production_agent_deps" src/backend/chat src/backend/evals 2>/dev/null || true); do
  echo "--- $f"
  rg -n "production_agent_deps|class .*Deps|`@dataclass`|dataclass|web_search_enabled|conversation_history|history|tool_output" "$f" -C 5
done

Repository: suitenumerique/conversations

Length of output: 19321


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "production_agent.py outline/relevant:"
ast-grep outline src/backend/chat/evals/production_agent.py --view expanded || true
sed -n '1,220p' src/backend/chat/evals/production_agent.py | cat -n

echo
echo "chat clients/pydantic_ai/agents relevant deps/run definitions:"
awk 'NR>=1 && NR<=320 { print NR ": " $0 }' src/backend/chat/agents/conversation.py | cat -n
echo
rg -n "_context_deps|ContextDeps|class ContextDeps|web_search_enabled|run\(|result\.messages|new_messages|messages|history|conversation_history|run_sync|deps" src/backend/chat/agents src/backend/chat/clients src/backend/chat/evals/production_agent.py -C 4

echo
echo "Pydantic AI package availability/version:"
python3 - <<'PY'
import importlib.util
for name in ["pydantic_ai", "pydantic_ai.main"]:
    spec = importlib.util.find_spec(name)
    print(name, "FOUND" if spec else "NO_SPEC", spec.origin if spec else "")
try:
    import pydantic_ai
    print("pydantic_ai.__version__", getattr(pydantic_ai, "__version__", "<no version>"))
    import inspect
    print("run signature", inspect.signature(pydantic_ai.Agent.run))
    print("main Agent.py file", inspect.getfile(pydantic_ai.Agent))
except Exception as e:
    print("pydantic_ai import error:", type(e).__name__, e)
PY

Repository: suitenumerique/conversations

Length of output: 50384


🌐 Web query:

Pydantic AI Agent.run deps mutable context concurrency thread safety run_sync docs

💡 Result:

In PydanticAI, agent runs are safe for concurrency [1]. You can concurrently execute agent runs (e.g., using asyncio.TaskGroup) without issues [1]. Regarding dependencies, context, and concurrency: Dependencies and Context Dependencies are passed through the RunContext object, which is accessed as the first parameter of system prompt functions, tools, and output validators [2][3]. These functions execute within the asynchronous context of an agent run [2][4]. If a dependency function is synchronous, PydanticAI runs it in a thread pool using asyncio.loop.run_in_executor [2][4]. Mutable Context While agent runs are concurrency-safe, developers must manage state carefully. If your deps object contains mutable data shared across concurrent agent runs, you must ensure that your own code handles thread safety (e.g., via locks), as PydanticAI does not automatically synchronize access to custom dependency objects [2][1]. Run vs. Run_sync Agents always execute within an asynchronous context [2][4]. run_sync is a synchronous wrapper around the run method, which internally executes loop.run_until_complete(self.run()) [5]. Consequently, whether you use synchronous or asynchronous dependencies is independent of whether you invoke the agent via run or run_sync [2][4]. Concurrency Limiting You can control the volume of concurrent operations using the max_concurrency parameter on the Agent constructor [6][5][7]. This supports: - An integer: A simple limit on concurrent operations [8][7]. - A ConcurrencyLimit: Advanced configuration providing optional backpressure (e.g., queue size limits) [8][5]. - A ConcurrencyLimiter: An object to share limits across multiple agents or models [8][7]. If the concurrency limit is reached, calls to run() or iter() will wait; if a max_queued limit is configured and exceeded, a ConcurrencyLimitExceeded exception is raised [6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect ChatConversation / service hooks and dependency fields:"
rg -n "class ChatConversation|model_post_init|last_message_at|updated_at|messaging|updated_by|ConversationAgent|add_agent_hooks|last_user_message_at|user_is_typing|send_message|run_agent_async|run_agent|_run_agent|context_deps" src/backend/chat clients/agents models -g '*.py' -C 4

echo
echo "Focused files containing relevant hooks/deps:"
rg -l "model_post_init|user_is_typing|last_user_message_at|add_agent_hooks|ConversationAgent|AIAgentService|_context_deps" src/backend/chat src/backend/agents 2>/dev/null || true

Repository: suitenumerique/conversations

Length of output: 50385


Build deps inside each case run.

production_agent_deps(service) returns the shared mutable AIAgentService._context_deps, and a new ContextDeps is created only in _get_eval_session_conversation. Keeping this as one shared object lets future per-call state, such as temporary fields or DB/model changes through the conversation/user references, bleed between eval cases. Move deps = production_agent_deps(service); deps.web_search_enabled = False into run_agent, matching tool_selection.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/configs/faithfulness_rag.py` around lines 71 - 87,
Move the production_agent_deps(service) call and web_search_enabled assignment
from make_faithfulness_rag_task_fn into run_agent so each eval case receives
fresh per-call dependencies. Keep the agent and tool-stub setup unchanged, and
ensure run_agent passes the newly created deps to agent.run.

Comment on lines +111 to +123
tool_output: |
{
"web_search": {
"0": {
"url": "https://example.com/gouvernement-france-2026",
"title": "Gouvernement de la République française",
"snippets": [
"Le premier ministre de la République française est François Bayrou depuis décembre 2024.",
"Il dirige le gouvernement sous la présidence d'Emmanuel Macron."
]
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stub states a stale officeholder.

The rest of the dataset is dated mid-2026, but this snippet asserts Bayrou as PM "depuis décembre 2024". Sébastien Lecornu has served as Prime Minister since September 2025, appointed after the Bayrou government fell to a no-confidence vote, and he was reappointed on 10 October 2025. The span assertion still passes, but feeding an incorrect "current" fact to the agent undercuts the case's intent and will age poorly.

Consider making the snippet timeline-neutral (e.g. a generic "Le premier ministre en exercice est X depuis " placeholder consistent with the 2026 framing) so the fixture doesn't need factual upkeep.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/datasets/tool_selection.yaml` around lines 111 - 123,
Update the web_search tool_output fixture in the dataset entry to remove the
stale François Bayrou officeholder claim and use a timeline-neutral placeholder
consistent with the mid-2026 framing. Preserve the existing JSON structure, URL,
title, and span-oriented evaluation behavior while avoiding a specific current
Prime Minister or date that requires future factual updates.

Comment on lines +23 to +24
def evaluate(self, ctx: EvaluatorContext) -> EvaluationReason:
response_urls = _extract_urls(ctx.output)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard ctx.output like the other inputs.

Every other source is isinstance(..., str)-checked, but a non-string ctx.output makes re.findall raise TypeError and turns the case into an evaluator error.

🛡️ Proposed fix
     def evaluate(self, ctx: EvaluatorContext) -> EvaluationReason:
-        response_urls = _extract_urls(ctx.output)
+        response_urls = _extract_urls(ctx.output) if isinstance(ctx.output, str) else set()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def evaluate(self, ctx: EvaluatorContext) -> EvaluationReason:
response_urls = _extract_urls(ctx.output)
def evaluate(self, ctx: EvaluatorContext) -> EvaluationReason:
response_urls = _extract_urls(ctx.output) if isinstance(ctx.output, str) else set()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/evaluators/url_regex.py` around lines 23 - 24, Update
evaluate in the URL regex evaluator to validate ctx.output with an
isinstance(..., str) guard before calling _extract_urls, matching the handling
of other inputs. Ensure non-string output follows the evaluator’s existing
non-match/reasoning path instead of reaching re.findall and raising TypeError.

Comment on lines +71 to +99
def _get_eval_session_user() -> User:
"""Return a single eval user reused for the lifetime of the Python process."""
global EVAL_SESSION_USER # pylint: disable=global-statement
if EVAL_SESSION_USER is not None:
return EVAL_SESSION_USER

EVAL_SESSION_USER, _ = User.objects.get_or_create(
sub=_EVAL_USER_SUB,
defaults={
"email": "eval-session@localhost",
"full_name": "Eval Session",
"allow_smart_web_search": True,
"password": make_password(None),
},
)
return EVAL_SESSION_USER


def _get_eval_session_conversation(user: User) -> chat_models.ChatConversation:
"""Return a single conversation for the eval session user."""
global EVAL_SESSION_CONVERSATION # pylint: disable=global-statement
if EVAL_SESSION_CONVERSATION is not None:
return EVAL_SESSION_CONVERSATION

EVAL_SESSION_CONVERSATION, _ = chat_models.ChatConversation.objects.get_or_create(
owner=user,
title=_EVAL_CONVERSATION_TITLE,
)
return EVAL_SESSION_CONVERSATION

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how AIAgentService loads/persists conversation history, and whether
# run_evals.py resets it or parallelizes case execution.
rg -nP -C5 '_run_agent|message_history' src/backend/chat/clients/pydantic_ai.py
rg -nP -C5 'asyncio\.gather|ThreadPoolExecutor|async def' src/backend/chat/management/commands/run_evals.py
rg -nP -C3 'ChatMessage\.objects|\.messages\.' src/backend/chat/clients/pydantic_ai.py

Repository: suitenumerique/conversations

Length of output: 4058


🏁 Script executed:

#!/bin/bash
set -u

echo "== file sizes =="
wc -l src/backend/chat/commands/run_evals.py 2>/dev/null || true
wc -l src/backend/chat/management/commands/run_evals.py 2>/dev/null || true
wc -l src/backend/chat/evals/production_agent.py 2>/dev/null || true

echo "== candidate files =="
fd 'run_evals\.py|production_agent\.py|pydantic_ai\.py' src/backend/chat | sed -n '1,80p'

echo "== run_evals relevant sections =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/backend/chat/management/commands/run_evals.py')
if not p.exists():
    p=Path('src/backend/chat/commands/run_evals.py')
data=p.read_text()
lines=data.splitlines()
for name, start, end in [('run_async_or_sync',180,290),('run_evals',290,380),('parallel',0,440)]:
    print(f'--- {name} {start}-{end} ---')
    for i in range(start,end+1):
        if 0 < i <= len(lines):
            print(f'{i:5}: {lines[i-1]}')
PY

echo "== production_agent relevant sections =="
p=Path('src/backend/chat/evals/production_agent.py')
data=p.read_text()
lines=data.splitlines()
for start,end in [(60,150),(150,240),(240,460)]:
    print(f'--- {start}-{end} ---')
    for i in range(start,end+1):
        if 0 < i <= len(lines):
            print(f'{i:5}: {lines[i-1]}')

echo "== pydantic_ai relevant sections =="
p=Path('src/backend/chat/clients/pydantic_ai.py')
data=p.read_text()
lines=data.splitlines()
for start,end in [(1510,1575)]:
    print(f'--- {start}-{end} ---')
    for i in range(start,end+1):
        if 0 < i <= len(lines):
            print(f'{i:5}: {lines[i-1]}')

echo "== usages of constants/helpers/models =="
rg -n "_get_eval_session_user|_get_eval_session_conversation|EVAL_SESSION_USER|EVAL_SESSION_CONVERSATION|_EVAL_USER_SUB|_EVAL_CONVERSATION_TITLE|reset_eval_session_cache|ChatMessage.objects|\.messages" src/backend/chat/evals src/backend/chat

Repository: suitenumerique/conversations

Length of output: 19148


🏁 Script executed:

#!/bin/bash
set -u

echo "== production_agent relevant sections =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/backend/chat/evals/production_agent.py')
data=p.read_text()
lines=data.splitlines()
for start,end in [(1,199), (150,199)]:
    print(f'--- {start}-{end} ---')
    for i in range(start,end+1):
        print(f'{i:5}: {lines[i-1]}')
PY

echo "== pydantic_ai relevant sections =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/backend/chat/clients/pydantic_ai.py')
data=p.read_text()
lines=data.splitlines()
for start,end in [(280,340),(1510,1620)]:
    print(f'--- {start}-{end} ---')
    for i in range(start,end+1):
        print(f'{i:5}: {lines[i-1]}')
PY

echo "== model definition and message persistence =="
fd 'models\.py' src/backend/chat -x sh -c 'echo "--- $1 ---"; sed -n "1,220p" "$1"' sh {}

echo "== call sites for run prompt with conversation =="
rg -n "def run\(|_get_eval_session_(user|conversation)|MessageHistory|history|messages|iterate_history|ChatMessage" src/backend/chat/agents src/backend/chat/clients

Repository: suitenumerique/conversations

Length of output: 50384


Reset or recreate the eval conversation before each make eval run.

build_production_agent_service() reuses one _EVAL_CONVERSATION_TITLE/_EVAL_USER_SUB _ conversation via get_or_create, and run_evals.py calls _agent.run(prompt) without passing message_history=[]. Since AIAgentService._prepare_agent_run() loads conversation.pydantic_messages, each separate invocation after a completed eval case will start with the previous run’s chat history. Either create a fresh conversation per eval case/run or clear/reset this fixed conversation before running cases.

Also make the module-global creation path safe, e.g. get_or_create() inside a lock/synchronization primitive, because parallel case execution could duplicate the read-then-create check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/evals/production_agent.py` around lines 71 - 99, Reset or
recreate the eval conversation before each eval case/run so
_get_eval_session_conversation does not reuse prior pydantic_messages; update
the surrounding eval setup or run flow while preserving the shared eval user
behavior. Also synchronize the module-global initialization in
_get_eval_session_user and _get_eval_session_conversation, including their
get_or_create paths, so parallel execution cannot duplicate creation.

Comment on lines +16 to +25
def handle(self, *args, **options):
output_path = generate_dashboard()
# The command runs inside Docker where BASE_DIR is /app; show the
# host-side path (repo checkout) so the user can open the file directly.
display_path = Path("src/backend") / output_path.relative_to(settings.BASE_DIR)
self.stdout.write(
self.style.SUCCESS(
f"Dashboard written to {display_path}. Open it in a browser to compare runs."
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

relative_to can raise for a purely cosmetic path.

If output_path isn't under settings.BASE_DIR (running outside the container, different BASE_DIR), Line 20 raises ValueError after the dashboard was already written successfully. Fall back to the absolute path.

🛡️ Proposed guard
-        display_path = Path("src/backend") / output_path.relative_to(settings.BASE_DIR)
+        try:
+            display_path = Path("src/backend") / output_path.relative_to(settings.BASE_DIR)
+        except ValueError:
+            display_path = output_path
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle(self, *args, **options):
output_path = generate_dashboard()
# The command runs inside Docker where BASE_DIR is /app; show the
# host-side path (repo checkout) so the user can open the file directly.
display_path = Path("src/backend") / output_path.relative_to(settings.BASE_DIR)
self.stdout.write(
self.style.SUCCESS(
f"Dashboard written to {display_path}. Open it in a browser to compare runs."
)
)
def handle(self, *args, **options):
output_path = generate_dashboard()
# The command runs inside Docker where BASE_DIR is /app; show the
# host-side path (repo checkout) so the user can open the file directly.
try:
display_path = Path("src/backend") / output_path.relative_to(settings.BASE_DIR)
except ValueError:
display_path = output_path
self.stdout.write(
self.style.SUCCESS(
f"Dashboard written to {display_path}. Open it in a browser to compare runs."
)
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/management/commands/generate_eval_dashboard.py` around lines
16 - 25, Update the path formatting in handle around
output_path.relative_to(settings.BASE_DIR) to catch cases where output_path is
outside settings.BASE_DIR, preserving the successful dashboard generation and
falling back to displaying output_path’s absolute path. Keep the existing
host-side display path when relative_to succeeds.

Comment on lines +78 to +108
def _validate_completion(self, response: chat.ChatCompletion) -> _ChatCompletion:
"""Normalize Albert API quirks before validation.

Albert's OpenAI-compatible API has two known non-conformances:
1. tool_calls[].type may not be 'function' — normalized to 'function',
unless the tool call is a genuine custom tool call (type='custom'
with a `custom` payload, which the openai SDK requires).
2. On multi-turn tool-call conversations, the second response sometimes
returns a non-standard `object` value and a non-list `choices` field.
Both are normalized before passing to _ChatCompletion.model_validate().
"""
data = response.model_dump()

if data.get("object") != "chat.completion":
data["object"] = "chat.completion"

if not isinstance(data.get("choices"), list):
data["choices"] = []

for choice in data.get("choices") or []:
for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
if not isinstance(tool_call, dict):
continue
# 'custom' is only valid with a `custom` payload; any other
# type (including 'custom' with a function payload) must be
# 'function' to pass the openai SDK union validation.
is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
if not is_custom and tool_call.get("type") != "function":
tool_call["type"] = "function"

return _ChatCompletion.model_validate(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Reduce _validate_completion complexity to unblock SonarCloud.

The quality gate reports cognitive complexity 16 where 15 is allowed. Extract tool-call type normalization into a small helper.

Proposed refactor
+    `@staticmethod`
+    def _normalize_tool_call_type(tool_call: dict[str, Any]) -> None:
+        is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
+        if not is_custom and tool_call.get("type") != "function":
+            tool_call["type"] = "function"
+
     def _validate_completion(self, response: chat.ChatCompletion) -> _ChatCompletion:
         ...
         for choice in data.get("choices") or []:
             for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
                 if not isinstance(tool_call, dict):
                     continue
-                is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
-                if not is_custom and tool_call.get("type") != "function":
-                    tool_call["type"] = "function"
+                self._normalize_tool_call_type(tool_call)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _validate_completion(self, response: chat.ChatCompletion) -> _ChatCompletion:
"""Normalize Albert API quirks before validation.
Albert's OpenAI-compatible API has two known non-conformances:
1. tool_calls[].type may not be 'function'normalized to 'function',
unless the tool call is a genuine custom tool call (type='custom'
with a `custom` payload, which the openai SDK requires).
2. On multi-turn tool-call conversations, the second response sometimes
returns a non-standard `object` value and a non-list `choices` field.
Both are normalized before passing to _ChatCompletion.model_validate().
"""
data = response.model_dump()
if data.get("object") != "chat.completion":
data["object"] = "chat.completion"
if not isinstance(data.get("choices"), list):
data["choices"] = []
for choice in data.get("choices") or []:
for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
if not isinstance(tool_call, dict):
continue
# 'custom' is only valid with a `custom` payload; any other
# type (including 'custom' with a function payload) must be
# 'function' to pass the openai SDK union validation.
is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
if not is_custom and tool_call.get("type") != "function":
tool_call["type"] = "function"
return _ChatCompletion.model_validate(data)
`@staticmethod`
def _normalize_tool_call_type(tool_call: dict[str, Any]) -> None:
is_custom = tool_call.get("type") == "custom" and "custom" in tool_call
if not is_custom and tool_call.get("type") != "function":
tool_call["type"] = "function"
def _validate_completion(self, response: chat.ChatCompletion) -> _ChatCompletion:
"""Normalize Albert API quirks before validation.
Albert's OpenAI-compatible API has two known non-conformances:
1. tool_calls[].type may not be 'function'normalized to 'function',
unless the tool call is a genuine custom tool call (type='custom'
with a `custom` payload, which the openai SDK requires).
2. On multi-turn tool-call conversations, the second response sometimes
returns a non-standard `object` value and a non-list `choices` field.
Both are normalized before passing to _ChatCompletion.model_validate().
"""
data = response.model_dump()
if data.get("object") != "chat.completion":
data["object"] = "chat.completion"
if not isinstance(data.get("choices"), list):
data["choices"] = []
for choice in data.get("choices") or []:
for tool_call in (choice.get("message") or {}).get("tool_calls") or []:
if not isinstance(tool_call, dict):
continue
self._normalize_tool_call_type(tool_call)
return _ChatCompletion.model_validate(data)
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 78-78: Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=suitenumerique_conversations&issues=AZ-pMcMBIp65em4OuZeQ&open=AZ-pMcMBIp65em4OuZeQ&pullRequest=481

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/providers/albert_models.py` around lines 78 - 108, Reduce
cognitive complexity in _validate_completion by extracting the tool-call type
normalization loop and its custom-payload condition into a small helper. Have
_validate_completion invoke the helper for each valid choice while preserving
the existing handling of malformed choices, tool calls, and genuine custom tool
calls.

Source: Linters/SAST tools

Comment on lines +47 to +49
assert len(comparison.coverage_gaps) == 2
assert comparison.has_regression_failures
assert len(comparison.regressions) == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep missing cases out of regressions.

A case absent from the after run is a coverage gap, not a case that “fail[s] after.” This assertion locks in double-counting for previously passing cases; represent after_passed as unknown and exclude coverage gaps from regressions.

Proposed test adjustment
     assert len(comparison.coverage_gaps) == 2
     assert comparison.has_regression_failures
-    assert len(comparison.regressions) == 1
+    assert not comparison.regressions
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert len(comparison.coverage_gaps) == 2
assert comparison.has_regression_failures
assert len(comparison.regressions) == 1
assert len(comparison.coverage_gaps) == 2
assert comparison.has_regression_failures
assert not comparison.regressions
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/chat/tests/evals/test_compare.py` around lines 47 - 49, Update
the comparison fixture and assertions around comparison.coverage_gaps and
comparison.regressions so cases missing from the after run are represented with
after_passed unknown and excluded from regressions. Preserve coverage_gaps for
those cases, and adjust the expected regression count to verify previously
passing missing cases are not double-counted.

Comment on lines +745 to +748
"""You are l’Assistant IA, a conversational assistant deployed by the DINUM
(Direction interministérielle du numérique) for French public servants
(agents publics), based on Mistral Medium 2508 and hosted on sovereign
infrastructure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hardcoded model identity in the default prompt will be wrong on other deployments.

The served model is resolved from LLM_DEFAULT_MODEL_HRID/LLM_CONFIGURATIONS, so pinning "based on Mistral Medium 2508" into the default instructions makes the assistant state a false identity anywhere else — and it directly conflicts with the "never present an assumption as a fact" rule below. The about_model case in datasets/tool_selection.yaml exercises this path.

📝 Proposed fix
-(Direction interministérielle du numérique) for French public servants
-(agents publics), based on Mistral Medium 2508 and hosted on sovereign
-infrastructure.
+(Direction interministérielle du numérique) for French public servants
+(agents publics), hosted on sovereign infrastructure.

Deployment-specific model naming is better sourced from the model configuration (or the self_documentation tool) than from a static default.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""You are lAssistant IA, a conversational assistant deployed by the DINUM
(Direction interministérielle du numérique) for French public servants
(agents publics), based on Mistral Medium 2508 and hosted on sovereign
infrastructure.
"""You are lAssistant IA, a conversational assistant deployed by the DINUM
(Direction interministérielle du numérique) for French public servants
(agents publics), hosted on sovereign infrastructure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/conversations/settings.py` around lines 745 - 748, Remove the
hardcoded “Mistral Medium 2508” identity from the default prompt in the
assistant instructions, while preserving the deployment-neutral description and
the existing prohibition against presenting assumptions as facts. Ensure
model-specific identity is obtained from the resolved LLM configuration or the
self_documentation tool so the about_model path reflects the active deployment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨(evals) behavioral eval framework for ConversationAgent

3 participants